A complete teaching repository for fundamentals of HTML, CSS, and JavaScript using a single-page product-style landing page.
This project is intentionally simple in tooling and rich in learning value. It demonstrates how to build and reason about real UI behavior without frameworks, while introducing patterns that map directly to modern frontend development.
- Project Overview
- What Students Build
- Learning Outcomes
- Tech Stack and Philosophy
- Repository Structure
- How to Run the Project
- Detailed Code Walkthrough
- UI and Interaction Flows
- Accessibility and UX Notes
- State, Data, and Rendering Model
- Validation Logic Explained
- Responsive Design Strategy
- Persistence with localStorage
- Teaching Plan and Classroom Flow
- Hands-On Exercises
- Manual Test Checklist
- Common Issues and Troubleshooting
- Extension Ideas
- Known Limitations in Current Implementation
- Suggested Refactors for Advanced Learners
- Glossary
- Contributing for Instructors
- License
This repo contains a compact frontend lab that teaches core web development concepts by building an interactive single-page interface with:
- A responsive header with mobile navigation toggle
- A hero area with dynamic active-user ticker
- Tabbed feature content driven by JavaScript state
- Pricing cards rendered from data with monthly and annual billing switch
- A sign-up form with client-side validation and inline errors
- Saved UI preferences using
localStorage
The lab is designed to help students move from static pages to state-driven UI thinking.
Students build and understand a page that includes:
- Semantic layout sections:
header,main,section,footer - Reusable CSS component classes and design tokens
- Dynamic DOM updates using
innerHTMLand event listeners - Event delegation for scalable interaction handling
- Form input validation and success/error messaging
- Simple persistent preferences across reloads
By the end, learners should be comfortable reading and writing code that connects data, state, rendering, and user interaction.
After completing this lab, students should be able to:
- Explain semantic HTML structure and why it matters.
- Build responsive layouts with CSS Grid, Flexbox, and media queries.
- Use CSS custom properties (
:rootvariables) for maintainable styling. - Model UI behavior with a central JavaScript
stateobject. - Render UI based on current state rather than hardcoded markup.
- Attach and reason about event listeners for click/change/submit flows.
- Validate form inputs with practical rules and user feedback.
- Persist selected preferences in
localStorageand reload safely. - Understand how vanilla JS patterns transfer to React/Next.js concepts.
- HTML5
- CSS3
- Vanilla JavaScript (ES6+)
- Browser APIs (
localStorage, DOM, events)
- No build tools required.
- No frameworks required.
- Simple enough for beginners.
- Structured enough to teach professional frontend habits.
This makes the repo ideal for early labs, bootcamp modules, and fundamentals review sessions.
fs-html-css-js-base-lab/
├── index.html # Page structure and semantic content
├── styles.css # Design tokens, layout, components, responsiveness
└── app.js # State, data, rendering, events, validation, persistence
index.html: Defines all major UI sections and accessibility attributes.styles.css: Defines visual language, spacing system, component styles, and breakpoints.app.js: Drives interactive behavior and keeps UI synchronized with app state.
- Clone or download the repository.
- Open
index.htmlin a browser. - Interact with tabs, billing toggle, pricing buttons, and form.
If you already use a local server tool (for example, VS Code Live Server), serve the folder and open the served URL. This is useful for classroom demos and consistent reload behavior.
- Modern browser (Chrome, Edge, Firefox, Safari)
- No package installation required
The HTML file is organized into reusable conceptual components:
- Site Header
- Logo link
- Mobile menu button with ARIA attributes
- Primary navigation links to page sections
- Hero Section
- Main headline
- Introductory teaching copy
- Two call-to-action buttons
- Dynamic stat card (
#liveUsers)
- Features Section
- Tab list (
role="tablist") - Tab buttons with
data-tabkeys - Empty tab panel (
#tabPanel) populated by JS
- Pricing Section
- Billing toggle (
#billingToggle) - Empty card container (
#pricingCards) rendered from pricing data
- Signup Section
- Form (
#signupForm) withnovalidate - Inputs for name, email, and track
- Inline error targets (
data-error-for) - Status message (
#formMsg)
- Footer
- Session context text
- Shows semantic landmarks clearly.
- Keeps dynamic containers explicit.
- Demonstrates form labeling and error mapping.
- Provides section IDs for navigation and smooth scrolling.
The stylesheet is structured around maintainability and reuse.
Defined in :root:
- Color system (
--bg,--card,--text,--muted,--line) - Radius (
--r) - Spacing scale (
--s1to--s6) - Max content width (
--max) - Shared shadow (
--shadow)
This is a practical intro to design systems.
box-sizing: border-box- Margin/padding reset on
html, body - Base typography and link behavior
.sr-onlyutility for accessible hidden text
- Sticky header with translucent background and backdrop blur
- Grid-based hero layout
- Reusable button system (
.Btn,.Btn--primary,.Btn--ghost) - Generic section card treatment
- Tabs styling with active state class
- Pricing card grid
- Form field and inline error styling
- Footer styling
@media (max-width: 900px):- Hero collapses to one column
- Pricing cards stack vertically
@media (max-width: 720px):- Hamburger button appears
- Nav becomes hidden popover
.Nav.is-opencontrols visibility
This gives students a clear, realistic responsive strategy.
The JS file follows a highly teachable architecture.
const state = {
billing: "monthly",
activeTab: "speed",
};State is the single source of truth for key UI behavior.
The script grabs essential elements once (menuBtn, nav, tabs, tabPanel, billingToggle, pricingCards) and reuses them.
featureCopy: content for each tabpricing: plan list with monthly/annual prices and perks
This demonstrates separation of data from presentation.
savePref()andloadPref()for persistencescrollToId(id)for smooth section navigation
renderNavButton(open)renderTabs()renderBillingToggle()renderPricing()renderAll()
Each function updates one part of the UI from state.
- Menu button toggles mobile nav
- Tab buttons update
state.activeTaband rerender tabs - Billing toggle updates
state.billingand rerenders pricing - Pricing card clicks use event delegation and update form message
setError(fieldName, message)isValidEmail(email)validate(values)
Checks include:
- Name length at least 2 characters
- Valid email pattern and no consecutive dots
- Required track selection
startLiveUsersTicker() updates #liveUsers every 1.5 seconds with bounded random change.
loadPref()renderAll()startLiveUsersTicker()
This initializes persisted state and paints UI correctly on load.
- User clicks menu button.
is-openclass toggles on nav.aria-expandedsyncs with actual state.
- User clicks a tab button.
state.activeTabupdates.- Tab panel content rerenders from
featureCopy. - Active button class/ARIA updates.
- User toggles checkbox.
state.billingswitches between monthly/annual.- Pricing cards rerender with matching price and suffix.
- User clicks
Choose <Plan>. - Event delegation identifies selected plan.
- Informational message shown near form.
- Page scrolls to signup section.
- User submits form.
- Validation runs and writes inline errors.
- If valid, success message displays and form resets.
Positive patterns already present:
- Semantic sectioning and landmarks
- ARIA on menu button and tablist
- Screen-reader-only helper class
- Inline field error slots per input
- Status region for form feedback (
role="status") - Keyboard-friendly native controls (buttons, input, select)
Teaching opportunity:
- Discuss why
aria-expandedmust always match visible nav state. - Explain
novalidateas a learning choice for custom validation practice.
This repo is an excellent "pre-framework" exercise.
The mental model is:
- Keep canonical UI facts in state.
- Store text/plan content as plain data.
- Render UI from state and data.
- Let events mutate state.
- Re-render the affected part.
That is the same core architecture students later use in component frameworks.
User action -> Event handler -> Update state -> Render function -> Updated DOM
Students avoid scattered one-off DOM changes and learn predictable UI logic.
The form validation combines user-friendly feedback and practical rules.
- Name rule: prevents empty or too-short names.
- Email rule: uses a baseline regex and rejects obvious bad formats like double dots.
- Track rule: enforces explicit selection.
Inline error rendering with data-error-for is a reusable pattern:
- Keep validation rules separate.
- Keep UI error display centralized.
- Avoid deeply coupling rule logic to specific DOM structures.
The CSS teaches responsive behavior through progressive adjustments:
- Desktop first for clarity.
- Collapse multi-column content at smaller widths.
- Introduce mobile-specific nav pattern only when needed.
Key ideas for learners:
- Breakpoints should respond to layout pressure, not random device names.
- Content hierarchy should remain intact across screen sizes.
- Interactive elements must remain reachable and usable on mobile.
The repo persists user preference state for:
- Selected tab
- Billing mode
Pattern used:
- Save only minimal necessary state.
- Parse safely with
try/catch. - Validate loaded values before trusting them.
- Render after loading.
This is a strong beginner-safe persistence pattern.
This structure works well for a 90 to 150 minute guided lab.
- Walk through section landmarks.
- Identify IDs used for navigation and JS targeting.
- Explain why some containers are intentionally empty and filled by JS.
- Show tokenized variables.
- Explain reusable component classes.
- Demonstrate responsive breakpoints and why they are chosen.
- Introduce state and data separation.
- Trace one render function end-to-end.
- Trace one event flow from click to rerender.
- Validate and submit flow.
- localStorage save/load cycle.
- Discuss reliability and graceful failure.
- Have students add a new tab and plan.
- Add one additional form rule.
- Improve code style or robustness without changing UX.
Use these exercises in order.
Goal:
- Add a fourth tab called
Security.
Tasks:
- Add a new tab button in HTML with
data-tab="security". - Add matching
featureCopy.securitydata in JS. - Verify tab rendering and active class behavior.
Learning focus:
- Data-driven rendering and button-state sync.
Goal:
- Add an
Enterpriseplan with custom perks.
Tasks:
- Add a new object to
pricingarray. - Confirm automatic rendering with existing map logic.
- Test click behavior and signup prompt.
Learning focus:
- Rendering lists from data and event delegation stability.
Goal:
- Prevent disposable/test domains.
Tasks:
- Extend
validate(values)with a blocked-domain check. - Show clear inline error message.
- Test accepted and rejected emails.
Learning focus:
- Extending business rules safely.
Goal:
- Allow left/right arrow keys to move between tabs.
Tasks:
- Add keydown listener on tab list.
- Cycle selected index.
- Update state and rerender.
Learning focus:
- Accessible keyboard interactions.
Goal:
- Close mobile nav when a nav link is clicked.
Tasks:
- Add click listener on nav container.
- Detect anchor clicks.
- Remove
is-openand syncaria-expanded.
Learning focus:
- State synchronization and UX details.
Use this checklist before marking lab completion.
- Page loads with styles and JS behavior active.
- Header, hero, features, pricing, signup, footer all visible.
- Mobile menu button appears below 720px.
- Menu opens/closes correctly.
-
aria-expandedupdates as menu state changes.
- Default tab content appears on load.
- Clicking each tab updates panel content.
- Active tab visual state and
aria-selectedare correct.
- Monthly pricing is default.
- Annual toggle updates all plan prices.
- Toggle state persists after reload.
- Clicking plan button updates signup message.
- Page scrolls to signup section.
- Empty submit shows errors.
- Invalid email shows email error.
- Missing track shows track error.
- Valid submit shows success message and resets fields.
- Active tab persists after reload.
- Billing mode persists after reload.
Possible cause:
app.jsfailed to load.
Checks:
- Ensure
<script src="./app.js" defer></script>is present. - Open browser console for runtime errors.
- Confirm file names match exact casing.
Possible cause:
- Stylesheet path issue.
Checks:
- Ensure
<link rel="stylesheet" href="./styles.css" />exists. - Confirm
styles.cssis in repository root.
Possible cause:
- Browser settings/private mode restrictions.
Checks:
- Test in normal browsing mode.
- Inspect Application/Storage tab in DevTools.
This is expected.
The lab focuses on client-side validation and UI feedback only. There is no backend integration in the current version.
These are high-value next steps for assignments or capstones.
- Connect form submission to a real API endpoint.
- Add loading, success, and error states for async requests.
- Add dark/light theme toggle persisted to localStorage.
- Replace
innerHTMLrendering with element creation for stronger safety. - Add unit tests for
isValidEmailandvalidate. - Add E2E checks for tabs, pricing toggle, and form flows.
- Add analytics hooks for plan and CTA clicks.
- Convert to modules and split by concerns.
- Rebuild this exact app in React while preserving behavior.
These are useful teaching points, not failures.
signupFormandformMsgare referenced directly without explicitgetElementByIddeclarations in JS.- This often works in browsers that expose element IDs as global variables, but it is brittle and not ideal for production reliability.
innerHTMLis used for convenience and readability in a lab context. This is acceptable here, but it requires care in real applications where content might come from untrusted input.- The tab UI uses ARIA roles but does not include full keyboard-arrow tab navigation logic yet.
- No automated tests are included.
- Explicitly bind form DOM elements in JS:
const signupForm = document.getElementById("signupForm");
const formMsg = document.getElementById("formMsg");- Extract tab rendering into smaller pure functions.
- Move repeated inline style attributes from HTML templates into CSS classes.
- Add a simple
render(state)orchestrator with section-level diffing strategy. - Introduce JS modules (
state.js,render.js,events.js,validation.js). - Add lightweight test runner setup (Vitest/Jest) for validation logic.
- State: The current values that control what the UI shows.
- Rendering: Updating the DOM to reflect current state/data.
- Event delegation: Attaching one parent listener to handle child element clicks efficiently.
- Design tokens: Reusable visual constants like colors and spacing variables.
- Semantic HTML: Meaningful structure elements that improve readability and accessibility.
- ARIA: Attributes that improve accessibility for assistive technologies.
- Progressive enhancement: Building baseline functionality first, then adding richer behavior.
If you use this repository for teaching, keep updates aligned to the learning goals.
Recommended contribution pattern:
- Keep each commit focused on one concept.
- Prefer small, teachable diffs with clear comments.
- Update this README whenever behavior changes.
- Add manual test steps for every new feature.
- Preserve beginner readability before advanced abstractions.
No license file is currently included in this repository.
If you plan to distribute or reuse this material publicly, add an explicit license (for example MIT) based on your institution or team policy.