A responsive product grid built with vanilla HTML, CSS, and JavaScript. No frameworks, no build tools, no external dependencies beyond a Google Font.
Goodies Grid is a front-end-only product catalog that demonstrates responsive layout, DOM manipulation, and interactive UI patterns using web standards exclusively.
- Responsive grid - 1 column on mobile, 2 on tablets, 3 on desktop
- Add to Cart toggle - visual state change with cart counter in the header
- Real-time search - debounced, case-insensitive filtering by product title
- Dynamic product creation - new cards injected via DOM APIs with full feature parity
- Image loading states - shimmer animation with fade-in on load
- Dark mode - automatic via
prefers-color-scheme - Reduced motion - respects
prefers-reduced-motionto disable all animations and transitions - Empty state - displayed when no products match the search query
index.html - Semantic markup and page structure
styles.css - Mobile-first styles, theming, animations
script.js - DOM interactions, search, dynamic card creation
favicon.svg - SVG favicon
img/ - Product images
Semantic elements are used throughout: <main>, <header>, <section>, and <article> for product cards. The script is loaded at the end of <body> so the DOM is parsed and ready when JavaScript executes, wrapped in an IIFE to avoid global scope pollution.
Mobile-first approach. Base styles target narrow viewports. Two min-width media queries progressively enhance the layout:
| Breakpoint | Grid columns |
|---|---|
< 768px |
1 |
>= 768px |
2 |
>= 1024px |
3 |
CSS Grid handles the product layout. Cards use Flexbox internally so the body content stretches and buttons align at the bottom regardless of description length.
Theming is managed entirely through CSS custom properties on :root, overridden inside a prefers-color-scheme: dark media query. No JavaScript is involved in theme switching.
BEM-inspired naming (product-card, product-card__title, product-card--added) keeps selectors flat and predictable.
All code runs inside an IIFE with "use strict". No globals are created.
Event delegation on the product grid container handles Add to Cart clicks for both static and dynamically created cards, avoiding per-button listeners.
Search uses a debounced input listener (300ms) to avoid excessive DOM traversal. A generation counter invalidates stale hide-transition callbacks when the user types faster than transitions complete.
Card hide/show uses CSS transitions (opacity, transform) with a transitionend listener and a fallback setTimeout to guarantee completion even when prefers-reduced-motion suppresses transitions. A done flag prevents duplicate execution from multiple transitionend events.
Dynamic cards are built exclusively with document.createElement() - no innerHTML. New cards are run through filterProducts() immediately so they respect the active search query.
- Semantic HTML -
<article>,<section>,<header>,<main>provide document structure for assistive technology - Skip link - "Skip to products" link appears on keyboard focus, allowing users to bypass controls and jump to the grid
- Visible
<label>for the search input (visually hidden but accessible to screen readers), replacingaria-label aria-labelon the product grid sectionaria-pressedon Add to Cart buttons communicates toggle state to screen readersaria-live="polite"on the cart count, result count, and empty state so changes are announced without interrupting the userfocus-visibleoutlines on all buttons for keyboard navigation without affecting mouse userstype="search"on the search input for semantic correctness and mobile keyboard optimization- Font loaded with
display=swapto avoid invisible text during font loading
- Debounced search - filtering runs only after 300ms of inactivity, reducing DOM operations during rapid typing
- Event delegation - a single click listener on the grid handles all cart buttons instead of one per card
- Shimmer animation cleanup - the shimmer
animationis stopped (style.animation = "none") once images load so the browser doesn't run invisible keyframe calculations - Image loading - the
--loadingclass is set in HTML (not added by JS) to prevent flash-of-loaded-image on cached assets;img.completeis checked to handle already-cached images synchronously - Native lazy loading -
loading="lazy"on below-the-fold images defers network requests until needed preconnecthints for Google Fonts reduce connection latency- Open Graph meta tags -
og:title,og:description,og:type, andog:imagefor social sharing - SVG favicon - resolution-independent, tiny file size, no build step required
- Forced scrollbar (
overflow-y: scroll) prevents layout shift from scrollbar appearance/disappearance during search filtering - No frameworks, no build tools - zero overhead; works by opening
index.htmlin a browser
- Product data and images are placeholders; no backend or persistent storage
- Cart state is in-memory only and resets on page reload
- The font (Jost) is loaded from Google Fonts - the only external resource; system fonts are used as fallback
- Images are served locally from
/img/and assumed to be available