🤖 AI-Readable JSON for this phase
All 10 sub-phases of Design System as JSON — perfect for AI agents.
{
"phase": {
"id": 2,
"name": "Design System",
"icon": "🎨",
"tagline": "Visual language for consistent UI",
"color": "#a78bfa",
"slug": "design-system"
},
"sub_phases": [
{
"id": "2.1",
"title": "Color Tokens (Semantic Palette)",
"icon": "🎨",
"description": "Map visual colors (gray-500, blue-600) to semantic roles (text-primary, surface-raised) so theme switching and white-labeling become data-attribute changes.",
"when_to_use": "Multi-theme product; white-label SaaS; design system spanning > 1 app; you need dark mode; brand refresh every 6 months",
"when_not_to_use": "Single-page landing with 5 elements; one-off marketing site; prototype that ships once and dies; no theming requirement",
"pros": [
"Theme switch = 1-line CSS change (`data-theme=dark`) — zero JS, zero rebuild;",
"Word 'primary' survives a brand refresh where 'blue' would force a 200-file rename;",
"WCAG contrast checks happen against semantic roles, not raw values — 1 audit covers every theme;",
"White-label fork from 100 → 1 codebase: client picks theme JSON at deploy time;",
"Designers and devs speak the same vocabulary in Figma Variables + code (token name parity);",
"Accessibility audits become mechanical: scan token→pair pairs, not 5000 hex literals."
],
"cons": [
"Indirection tax — juniors grep 'primary' and miss the actual value;",
"Token overlap ('accent' vs 'brand') confuses teams if not enforced in lint;",
"HSL/oklch math is required to derive shades from one base — wrong math = ugly ramps;",
"Tailwind already does this — adopting tokens on top adds a layer you must maintain;",
"Print/email fallbacks still need raw hex — tokens leak into non-browser contexts."
],
"gotchas": [
"Pure #000 on pure #fff hits WCAG AAA but feels harsh — use #0a0a14 / #f5f5f7 for softer contrast that still passes AA (4.5:1);",
"HSL hue rotation for variant shades shifts color identity — use oklch or HSL with locked hue; rotate lightness only for a true ramp;",
"Form controls (input, select, checkbox) ignore CSS background by default — explicit `accent-color` and `color-scheme: dark` are required;",
"iOS Safari auto-applies blue/yellow tint to inputs in dark mode unless `color-scheme: dark` is set on `:root`;",
"Images baked in light mode look like flashlights in dark — apply `filter: invert(1) hue-rotate(180deg)` to <img> or use SVG;",
"Charts (Chart.js, D3) hardcode colors — read tokens via getComputedStyle, don't import CSS into JS;",
"Transparent tokens (`--surface: hsl(... / 50%)`) need a separate `--surface-solid` for accessibility overlays or backgrounds stack visibly;",
"Shadow tokens don't theme — shadows look wrong on dark bg unless `--shadow-color` is also a token (alpha-included)."
],
"anti_patterns": [
"Naming tokens by color: `--blue-500` instead of `--color-primary-500` — they become useless the moment brand changes;",
"Defining 50 shades when the ramp needs 5 (50/100/200/500/900) — every shade is a future decision you forgot;",
"Storing tokens in JS only — sync drift between Figma and code; use CSS custom props or Style Dictionary;",
"Using hex when HSL/oklch would let you derive variants with one knob (lightness);",
"Skipping the 'danger/warning/info/success' family because 'the app never shows errors' — it will, in week 3."
],
"prereqs": [
"1.2"
],
"next_steps": [
"2.2",
"2.5",
"2.8"
],
"examples": [
"codehub-homepage",
"flowboard",
"stronghold-3d",
"habit-streak"
],
"snippet": ":root { --color-bg:hsl(240 10% 5%); --color-text-primary:hsl(0 0% 96%); --color-accent:hsl(330 81% 60%); }",
"snippet_full": ":root {\\n --color-bg: hsl(240 10% 5%);\\n --color-surface: hsl(240 10% 9%);\\n --color-text-primary: hsl(0 0% 96%);\\n --color-text-muted: hsl(240 5% 65%);\\n --color-accent: hsl(330 81% 60%);\\n --color-success: hsl(142 71% 45%);\\n --color-warning: hsl(38 92% 50%);\\n --color-danger: hsl(0 84% 60%);\\n --color-border: hsl(240 6% 20%);\\n}\\n[data-theme=\\\"light\\\"] {\\n --color-bg: hsl(0 0% 98%);\\n --color-surface: hsl(0 0% 100%);\\n --color-text-primary: hsl(240 10% 10%);\\n --color-text-muted: hsl(240 5% 40%);\\n}\\n:root { color-scheme: dark light; }\\n.btn { background: var(--color-accent); color: var(--color-text-primary); border: 1px solid var(--color-border); }",
"tags": [
"color",
"tokens",
"wcag",
"design-system",
"theme",
"สี",
"ธีม"
],
"difficulty": "Intermediate",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/--*",
"compare_with": [
"1.2 (raw CSS variables without semantic naming)",
"Tailwind config (built-in but JS-bound)",
"Style Dictionary (multi-platform export)"
]
},
{
"id": "2.2",
"title": "Typography Scale (Modular)",
"icon": "🔤",
"description": "Pick a ratio (1.250 / 1.333 / 1.414 / 1.5) and generate a type ramp xs→5xl with consistent visual rhythm — stops decision fatigue and survives redesigns.",
"when_to_use": "UI with > 3 distinct text sizes; design system spanning multiple pages; need consistent hierarchy in marketing + product UI; brand guidelines exist",
"when_not_to_use": "Single landing with hero + body + footer; print/PDF-only deliverable; pure icon UI with labels < 3 words",
"pros": [
"Visual hierarchy becomes automatic — designers can't accidentally pick a font-size that breaks the rhythm;",
"Math-driven ratio means you can add a new size (text-6xl) by multiplying, not guessing;",
"Tailwind/utility users get the ramp for free — text-sm/base/lg/xl/2xl matches the scale;",
"Line-height couples to font-size (tight for display, loose for body) — readability stays correct at every step;",
"Type tokens survive brand pivots: change ratio, regenerate ramp, ship in 30 min instead of 3 days;",
"Accessibility audits key off font-size only — semantic tokens let a11y scripts verify 16px minimum automatically."
],
"cons": [
"1.25 (Major Third) feels tight at large sizes; 1.5 feels airy — one ratio doesn't serve display + body;",
"Body must stay ≥ 16px or hit iOS auto-zoom on focus — ratios that start at 14px fail mobile;",
"Variable fonts can collapse the ramp into one weight axis — adopting scales duplicates what's already there;",
"Line-length (measure) is more important than size — a perfect scale with 200-char lines still fails reading tests;",
"Asian/CJK fonts have wider footprints — Latin scale numbers look cramped on Thai/Chinese body text."
],
"gotchas": [
"Headings < 16px on mobile trigger iOS Safari auto-zoom on input focus — keep input text ≥ 16px even if h6 is smaller;",
"rem scales with user browser font setting — a 1.5 ratio on a user-set 20px base ≠ the design; test with browser zoom 200%;",
"font-family swap (FOUT) reflows text — reserve width with `size-adjust` + `ascent-override` @font-face descriptors;",
"Variable font weight axis (wght) needs explicit `font-variation-settings: 'wght' 600` — `font-weight: 600` alone may not work;",
"Letter-spacing tightens on display sizes, loosens on captions — apply per token (`--tracking-tight: -0.02em`);",
"Korean/Thai have no ligatures — `font-feature-settings: 'liga' 1` enables complex shaping only where supported;",
"Don't mix serif display + sans body without a reason — two families need 2× the work in variable font licensing;",
"Tabular numerals (`font-variant-numeric: tabular-nums`) matter for prices, timers, tables — sans tables look jittery on resize."
],
"anti_patterns": [
"Using random pixel values (13px, 17px, 23px) — kills rhythm and confuses the next dev who has to extend it;",
"Picking a scale ratio without testing display sizes — 1.25 makes text-6xl too small for hero headlines;",
"Setting body < 16px on mobile — iOS Safari auto-zooms inputs and breaks layout;",
"Mixing units (px + rem + em) in one scale — rem for everything is the 2024 default;",
"Ignoring measure (line-length): 80 chars max for body, 30-40 for captions — perfect scale with bad measure still reads badly."
],
"prereqs": [
"1.2"
],
"next_steps": [
"2.4",
"2.5"
],
"examples": [
"stronghold-3d",
"karaoke-studio",
"codehub-homepage",
"knowledge-garden"
],
"snippet": ":root { --text-xs:.75rem; --text-sm:.875rem; --text-base:1rem; --text-lg:1.25rem; --text-xl:1.5rem; --text-2xl:1.875rem; --text-3xl:2.25rem; --text-4xl:3rem; }",
"snippet_full": ":root {\\n /* ratio 1.250 — Major Third, base 1rem (16px) */\\n --text-xs: 0.75rem; /* 12px */\\n --text-sm: 0.875rem; /* 14px — captions only, never body */\\n --text-base: 1rem; /* 16px — body min for mobile */\\n --text-lg: 1.25rem; /* 20px */\\n --text-xl: 1.563rem; /* 25px */\\n --text-2xl: 1.953rem; /* 31px */\\n --text-3xl: 2.441rem; /* 39px */\\n --text-4xl: 3.052rem; /* 49px */\\n --leading-tight: 1.15; /* display */\\n --leading-normal: 1.55; /* body */\\n --tracking-tight: -0.02em;\\n}\\nh1 { font-size: var(--text-4xl); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); }\\nbody { font-size: var(--text-base); line-height: var(--leading-normal); }\\ninput, select, textarea { font-size: max(1rem, 16px); } /* block iOS zoom */",
"tags": [
"typography",
"scale",
"type",
"type-ramp",
"อักษร",
"ฟอนต์",
"modular"
],
"difficulty": "Beginner",
"time_to_learn": "30 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/font-size",
"compare_with": [
"Tailwind text-* utilities (built-in 1.25 ramp)",
"Type Scale (type-scale.com) for visual math",
"Utopia.fyi (fluid clamp() generator)"
]
},
{
"id": "2.3",
"title": "Spacing Tokens (8pt Grid)",
"icon": "📏",
"description": "All margin/padding/gap values from a fixed scale (4/8/12/16/24/32/48/64/96) so every gap is divisible by 4 — pixel-snaps cleanly on retina and aligns across teams.",
"when_to_use": "Multi-dev team; design system; pixel-perfect mockups in Figma; want pixel-perfect comparison to design files",
"when_not_to_use": "Single-dev weekend prototype; canvas/3D game where units are world-space not screen-space; pixel-art where 1px = 1 unit",
"pros": [
"All values divisible by 4 → pixel-snaps to whole pixels on 1x, 2x, 3x displays (no fuzzy 1.5px gaps);",
"Dev ↔ designer parity: Figma '8pt grid' plugin produces the exact same token names as CSS variables;",
"Conversion to rem (16px base) becomes trivial: --space-4:1rem; --space-8:2rem; user-zoom respects scale;",
"Visual rhythm reads as intentional, not accidental — every gap has a reason because the scale is finite;",
"Tailwind's 4/8/16/24/32/48 ramp matches this 1:1 — migrating tokens to utilities is zero-work;",
"Catches rogue 13px / 17px / 23px paddings in code review — lint rule `margin: ! \\d+(px)` rejects them."
],
"cons": [
"8px is too coarse for tight UI (button icon gap of 6px would round to 4 or 8 and feel wrong);",
"Half-step tokens (--space-1: 4px, --space-2: 8px) still leak — some teams need 6px for icon-text baselines;",
"Some components naturally break the grid (1px borders, 0.5px hairlines) — tokens don't replace those;",
"Figma grids are 8pt but engineering reality includes iOS safe-area insets (34pt) that are off-grid;",
"Adopting on top of an existing codebase means sed-replacing 1000+ literals — migration cost is real."
],
"gotchas": [
"Padding inside small buttons (< 32px tall) needs --space-2 (8px), not --space-4 (16px) — the scale serves size, not just visual rhythm;",
"iOS safe-area-inset-* (notch, home indicator) returns 34pt / 21pt — off-grid by design; wrap in `env(safe-area-inset-bottom)` not raw token;",
"border-radius follows its own scale (--radius-sm/md/lg/pill) — coupling it to spacing makes pills wrong (9999px ≠ 64);",
"Stacked gaps with margin collapse: use `gap` on flex/grid (no collapse) instead of margin + padding pairs;",
"Negative margins for overlapping layouts (`margin-left: -16px`) need negative tokens (`--space-neg-4: -1rem`) or raw values;",
"Letter-spacing creates optical spacing — don't tighten scale to compensate for tracked text;",
"Charts: bar gap ≠ card gap ≠ list-item gap — domain-specific scale (--chart-bar-gap: 4px) sometimes beats global;",
"Vertical rhythm via `line-height: 1.5 * font-size` should hit grid — at 16px base, 24px line = --space-6, perfect alignment."
],
"anti_patterns": [
"Random 13/17/23 values — the whole point of the scale is to forbid these;",
"One token per layout (`--hero-padding: 87px`) — bespoke values break the system;",
"Mixing px and rem in one scale — pick rem (user-zoom-aware) and commit;",
"Defining --space-3, --space-5, --space-7 to fill gaps — every gap is a decision you have to defend;",
"Skipping radius/elevation tokens because 'spacing is enough' — those are separate scales that share the grid concept."
],
"prereqs": [
"1.2"
],
"next_steps": [
"2.4",
"2.10"
],
"examples": [
"codehub-homepage",
"stronghold-3d",
"flowboard"
],
"snippet": ":root { --space-1:4px; --space-2:8px; --space-4:16px; --space-6:24px; --space-8:32px; --space-12:48px; --radius-md:12px; --radius-pill:9999px; }",
"snippet_full": ":root {\\n --space-0: 0;\\n --space-1: 0.25rem; /* 4px */\\n --space-2: 0.5rem; /* 8px */\\n --space-3: 0.75rem; /* 12px */\\n --space-4: 1rem; /* 16px */\\n --space-6: 1.5rem; /* 24px */\\n --space-8: 2rem; /* 32px */\\n --space-12: 3rem; /* 48px */\\n --space-16: 4rem; /* 64px */\\n --radius-sm: 6px;\\n --radius-md: 12px;\\n --radius-lg: 20px;\\n --radius-pill: 9999px;\\n}\\n.card { padding: var(--space-4); border-radius: var(--radius-md); }\\n.btn { padding: var(--space-2) var(--space-4); gap: var(--space-2); }\\n.stack > * + * { margin-top: var(--space-4); }",
"tags": [
"spacing",
"grid",
"8pt",
"rhythm",
"ระยะ",
"กริด",
"tokens"
],
"difficulty": "Beginner",
"time_to_learn": "15 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding",
"compare_with": [
"Tailwind 4/8/16/24/32 default scale",
"Material Design 4dp/8dp grid",
"Bootstrap 5 spacing utilities"
]
},
{
"id": "2.4",
"title": "Component Library",
"icon": "🧱",
"description": "Build Button, Card, Modal, Input, Toast, Chip, Tabs, Tooltip once with variants (size/state/elevation) and reuse — atoms become the team's shared vocabulary.",
"when_to_use": "App > 3 pages; team ≥ 2 devs; any project that ships v2+; you find yourself copy-pasting the same button class",
"when_not_to_use": "Single-page marketing site; one-shot landing page; prototype that lives < 1 week; you have < 10 components total",
"pros": [
"Write button once, use 1000× — accessibility (focus ring, ARIA, keyboard) lives in one place, not 47 places;",
"Visual regression tests (Percy/Chromatic) lock one Button.png — every change is reviewed against baseline;",
"Onboarding new devs: teach 8 components, they ship features; don't teach 800 CSS rules;",
"Bug fix in one component fixes every screen — saves 10× the work a fragmented codebase costs;",
"Storybook/Histoire dev mode gives QA/designers a sandbox without engineering deploying;",
"Theme/variant via data-attributes (data-variant=danger, data-size=sm) — JS reads CSS, not the other way around."
],
"cons": [
"Up-front cost: 4–8 hours per component is normal before any page uses it — feels unproductive;",
"Over-abstraction: a 'universal' Form component that handles 12 use-cases becomes a 600-line monster nobody dares touch;",
"Variant explosion: Button × 5 sizes × 5 variants × 3 states = 75 CSS permutations to maintain;",
"Lock-in to choices made early (border vs shadow elevation) — pivoting is a 2-day rewrite, not a 1-hour swap;",
"Component-only thinking misses composition — a Modal is fine but a Modal+Toast+Form might have layout bugs you never tested."
],
"gotchas": [
"Use native `<dialog>` for modals — gets focus trap, ESC-to-close, backdrop for free; `showModal()` + `::backdrop` CSS;",
"Toast notifications need an aria-live region (`role=status` or `aria-live=polite`) — without it, screen readers miss them;",
"Tabs use `role=tablist` + `role=tab` + `aria-selected` + arrow-key navigation — manual is 30 lines, library is 1 import;",
"Inputs need explicit `<label for>` — placeholder-as-label fails accessibility audits and disappears on focus;",
"Disabled buttons (`disabled`) lose focusability — for 'loading' states use `aria-busy=true` + visually disable but keep focusable;",
"`<button>` vs `<a>`: use button for actions (no URL change), anchor for navigation (right-click → new tab works);",
"Focus rings: never `outline: none` without replacement — `:focus-visible` ring with 2px solid + offset is the modern default;",
"Shadow elevation: keep depth consistent — 1 layer of shadow + border beats 3 nested shadows that look muddy on retina."
],
"anti_patterns": [
"Component-per-pixel-figma: one component per mockup without extracting shared atoms (3 button styles become 3 components that should be 1 with variants);",
"Wrapper-only components: `<Card><Card.Body><Card.Title>` when a plain `<div class=card>` with utility classes is enough;",
"JS-driven everything: a button that needs JS to toggle a class is a `<details>` or checkbox under the hood;",
"Inconsistent naming: Btn/Button/PrimaryButton in same codebase — pick one (Button is the React-y default) and lint;",
"No prop API contract: component accepts 12 string props that should be 2 enums — over-flexibility is over-bug-surface."
],
"prereqs": [
"2.1",
"2.3"
],
"next_steps": [
"2.5",
"2.6"
],
"examples": [
"codehub-homepage",
"flowboard",
"knowledge-garden",
"eventtix"
],
"snippet": ".card { background:var(--color-surface); border-radius:var(--radius-lg); padding:var(--space-4); transition:transform .2s; } .card:hover { transform:translateY(-2px); }",
"snippet_full": "/* Button component with variants via data-attribute */\\n.btn {\\n display: inline-flex; align-items: center; gap: var(--space-2);\\n padding: var(--space-2) var(--space-4);\\n border: 1px solid transparent; border-radius: var(--radius-md);\\n font: inherit; font-weight: 600; cursor: pointer;\\n transition: background .15s, transform .1s;\\n}\\n.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\\n.btn[data-variant=\\\"primary\\\"] { background: var(--color-accent); color: white; }\\n.btn[data-variant=\\\"ghost\\\"] { background: transparent; color: var(--color-text-primary); }\\n.btn[data-size=\\\"sm\\\"] { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }\\n.btn[aria-busy=\\\"true\\\"] { opacity: .6; cursor: progress; }\\n/* Modal via native <dialog> */\\ndialog { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-6); background: var(--color-surface); }\\ndialog::backdrop { background: rgb(0 0 0 / 60%); backdrop-filter: blur(4px); }",
"tags": [
"components",
"library",
"design-system",
"ui",
"atomic",
"ชิ้นส่วน"
],
"difficulty": "Intermediate",
"time_to_learn": "4 hours",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog",
"compare_with": [
"Headless UI / Radix (unstyled accessible primitives)",
"shadcn/ui (copy-paste component recipes)",
"Storybook (visual dev environment for any lib)"
]
},
{
"id": "2.5",
"title": "Animation Library (Easing + Duration Tokens)",
"icon": "✨",
"description": "Tokenize easing curves (ease-out for enter, ease-in for exit) and durations (100/200/300/500ms) so every motion across the app feels consistent and respects prefers-reduced-motion.",
"when_to_use": "UI feels static; want premium feel; need visual feedback on hover/click; modals/sheets/toasts in/out",
"when_not_to_use": "Decoration only (bouncing logos that add no information); users with `prefers-reduced-motion: reduce`; data-dense dashboards where motion hides info; 60fps gameplay (use rAF, not CSS)",
"pros": [
"GPU-accelerated transforms (translate/scale/rotate) + opacity hit 60fps even on mid-range Android — width/height/margin animations don't;",
"Tokens for duration/easing mean every component speaks the same motion language — no awkward 173ms transitions;",
"`prefers-reduced-motion` media query is 1 line to disable — a11y audit passes, vestibular-disorder users are safe;",
"Easing > duration for perceived quality: same 250ms with `cubic-bezier(0.16,1,0.3,1)` feels 2× more premium than `ease`;",
"Web Animations API (WAAPI) lets you pause/seek/reverse without state libraries — `element.animate(keyframes, options)`;",
"Reduced motion fallback (duration: 0.01ms) doesn't lose state — opacity:0 element still appears, just instantly."
],
"cons": [
"Bouncy animations on primary CTAs delay action by 200ms — every delay is a UX tax on users in a hurry;",
"backdrop-filter + transform combo on Safari can drop to 15fps — test on real iPhone, not just Mac Safari;",
"Auto-playing animations on page load violate WCAG 2.2.2 — pause/play controls required for > 5s loops;",
"CSS @keyframes can't be triggered programmatically without adding/removing classes — JS event → class → animation is brittle;",
"Stagger animations (> 5 children with delays) pile up in compositor memory — laptops throttle after ~50 concurrent."
],
"gotchas": [
"Animate `transform` and `opacity` only — they run on the compositor; animating `top/left/width/height` triggers layout every frame;",
"`will-change: transform` is a hint, not a guarantee — overuse creates new compositor layers that exhaust GPU memory (200MB+);",
"`cubic-bezier(0.16, 1, 0.3, 1)` ('ease-out-expo') for enter, `cubic-bezier(0.7, 0, 0.84, 0)` ('ease-in-quart') for exit — symmetric ease looks mechanical;",
"`prefers-reduced-motion: reduce` must set animation-duration to ~0.01ms (not 0 — animation events may not fire at 0);",
"iOS Safari pauses CSS animations when tab is backgrounded — resume by listening to `visibilitychange` and re-applying class;",
"Transform on SVG elements uses different origin than HTML — `transform-origin: center` works, but nested SVG transforms compound;",
"Stagger via `transition-delay: calc(var(--i) * 50ms)` with `--i: 0..n` set inline — cleaner than nth-child arithmetic;",
"Scroll-driven animations (CSS Scroll-Linked Animations, 2024+) require `animation-timeline: scroll()` — not yet in Safari stable."
],
"anti_patterns": [
"Animating `box-shadow` for hover effects — triggers paint; animate `background` color or use a pre-rendered shadow layer;",
"Same duration for enter and exit (250ms in, 250ms out) — feels slower because user perceives exit first; use 200ms in / 150ms out;",
"Long animation chains: button press → ripple → modal open → toast slide → page transition = 1.5s of motion before user sees content;",
"Bounce/easing on text (letters wobble) — distracts from reading; reserve motion for spatial changes;",
"No reduced-motion fallback — a vestibular-disorder user literally cannot use your app, and an a11y audit will fail."
],
"prereqs": [
"2.4"
],
"next_steps": [],
"examples": [
"karaoke-studio",
"stronghold-3d",
"eventtix",
"flowboard"
],
"snippet": ":root { --ease-out:cubic-bezier(0.16,1,0.3,1); --duration-normal:250ms; } @media (prefers-reduced-motion:reduce) { *,*::before,*::after { transition-duration:.01ms!important; animation-duration:.01ms!important; } }",
"snippet_full": ":root {\\n --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* enter — fast start, soft land */\\n --ease-in: cubic-bezier(0.7, 0, 0.84, 0); /* exit — slow start, fast leave */\\n --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* spatial movement */\\n --duration-fast: 150ms;\\n --duration-normal: 250ms;\\n --duration-slow: 400ms;\\n}\\n@keyframes bounce-in {\\n 0% { opacity: 0; transform: scale(0.92); }\\n 60% { transform: scale(1.04); }\\n 100% { opacity: 1; transform: scale(1); }\\n}\\n.modal { animation: bounce-in var(--duration-normal) var(--ease-out) both; }\\n.btn:active { transform: scale(0.97); transition: transform 80ms var(--ease-in); }\\n@media (prefers-reduced-motion: reduce) {\\n *, *::before, *::after {\\n animation-duration: 0.01ms !important;\\n transition-duration: 0.01ms !important;\\n scroll-behavior: auto !important;\\n }\\n}",
"tags": [
"animation",
"motion",
"easing",
"duration",
"prefers-reduced-motion",
"อนิเมชัน",
"เคลื่อนไหว"
],
"difficulty": "Intermediate",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/animation",
"compare_with": [
"Framer Motion / Motion One (JS-driven, larger)",
"GSAP (timeline-based, expensive license)",
"Pure CSS @keyframes (zero-dep, what we recommend)"
]
},
{
"id": "2.6",
"title": "Icon System (Emoji + SVG)",
"icon": "🎯",
"description": "Pick emoji for zero-byte speed or inline SVG for crisp controllable icons — never both in one UI; currentColor in SVG makes them themable with no extra classes.",
"when_to_use": "Every UI; nav menus; status indicators; button labels; social/CTA icons",
"when_not_to_use": "Brand-specific icons that match a system (Material Symbols, Phosphor, Heroicons) — those bring consistency you don't want to fight; pure emoji-only apps where OS variance is acceptable",
"pros": [
"Emoji cost 0 bytes and inherit system font (no font-load blocking) — fastest possible icon;",
"Inline SVG with `fill='currentColor'` or `stroke='currentColor'` inherits text color — theme switching just works;",
"SVG scales to any size without raster artifacts; emoji at 96px+ looks pixely on some OS;",
"SVG `<path>` data is text — grep-able, version-controllable, optimizable via SVGO;",
"Single SVG sprite (1 HTTP request) reused via `<svg><use href=#icon-home>` beats 50 icon requests;",
"Stroke-width consistency (1.5 / 2) across an icon family reads as designed-by-one-hand; mixing weights looks amateur."
],
"cons": [
"Emoji render differently across OS — 🍑 on iOS ≠ Windows ≠ Android (color emoji vs text); brand suffers;",
"Inline SVG bloats HTML — 30 icons inline = 30KB in DOM (sprite + use is 30KB once, then 100B per use);",
"Emoji in <input> or <button> text aligns weirdly — `vertical-align: -0.1em` fixes most cases;",
"Lucide/Phosphor add 50–200KB — for 10 icons, hand-rolled inline SVG is smaller;",
"Emoji accessibility: screen readers read '🎉' as 'party popper' which is rarely the intent — wrap with `<span aria-label>`."
],
"gotchas": [
"Apple Color Emoji vs Segoe UI Emoji vs Noto Color Emoji — same codepoint renders 3 different glyphs; test on each;",
"SVG `width=20 height=20` attributes are CSS-overridable but the SVG won't resize if it has a viewBox without preserveAspectRatio tweak;",
"`<svg>` inside `<button>` is fine, but the SVG must have `aria-hidden=true` so screen readers don't announce 'image';",
"Stroke alignment: 24×24 grid with 2px stroke at center means visual weight differs from 2px stroke on 16×16; scale icons together;",
"Some emoji have hidden variation selectors (️ U+FE0F) that change text → emoji presentation — copy-paste from emoji keyboards, not codepoints;",
"Animated SVG icons (`<animate>` element inside SVG) trigger compositor differently than CSS — sometimes jankier on iOS;",
"Icon font ligatures (`:before { content: '\\e001' }`) lose to inline SVG on accessibility — no text fallback if font fails;",
"`<use href='#icon'>` requires CORS-friendly URLs for cross-origin SVGs — same-origin sprites work without headers."
],
"anti_patterns": [
"Mixing emoji + SVG in the same UI (🎵 for play but SVG for pause) — visual chaos; pick one and commit;",
"Icon font (FontAwesome) for 5 icons when inline SVG is 1KB total — 80KB of font for 5 glyphs is wasteful;",
"Random stroke widths (1px outline icon next to 3px filled icon) — pick a weight (1.5 / 2) and apply via stroke-width CSS var;",
"Color-locking icons (`fill='#000'`) — theming breaks; use `fill='currentColor'` and let CSS cascade;",
"Emoji-as-button: `👍` to 'like' is OS-dependent and screenshotted differently by every user — design a button, not an emoji vote."
],
"prereqs": [
"1.1"
],
"next_steps": [
"2.4"
],
"examples": [
"stronghold-3d",
"karaoke-studio",
"flowboard",
"mood-garden"
],
"snippet": "<svg width=20 height=20 viewBox='0 0 24 24' fill='none' stroke=currentColor stroke-width=2><path d='M12 5v14M5 12l7 7 7-7'/></svg>",
"snippet_full": "<!-- icons.svg — single sprite, fetch once and cache forever -->\\n<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" style=\\\"display:none\\\">\\n <symbol id=\\\"i-home\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\">\\n <path d=\\\"M3 12l9-9 9 9M5 10v10h14V10\\\"/>\\n </symbol>\\n <symbol id=\\\"i-search\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\">\\n <circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"7\\\"/><path d=\\\"m21 21-4.3-4.3\\\"/>\\n </symbol>\\n</svg>\\n<!-- usage: -->\\n<button class=\\\"btn\\\">\\n <svg class=\\\"icon\\\" aria-hidden=\\\"true\\\"><use href=\\\"#i-search\\\"/></svg>\\n <span>Search</span>\\n</button>\\n<style>.icon { width: 1.25em; height: 1.25em; }</style>",
"tags": [
"icons",
"svg",
"emoji",
"sprite",
"ไอคอน"
],
"difficulty": "Beginner",
"time_to_learn": "15 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use",
"compare_with": [
"Lucide (1500+ SVG icons, tree-shakeable)",
"Phosphor Icons (6 weights, flexible family)",
"Heroicons (Tailwind-made, MIT)",
"Material Symbols (Google, variable axes)"
]
},
{
"id": "2.7",
"title": "Responsive Breakpoints (Mobile-first)",
"icon": "📱",
"description": "Define min-width breakpoints (640/768/1024/1280) and write styles for mobile, then layer on `min-width` overrides — desktop styles inherit unless contradicted.",
"when_to_use": "Public-facing app; mobile traffic > 30%; SaaS that signs up users on phones; any B2C product",
"when_not_to_use": "Internal tool desktop-only; admin dashboard restricted to ops staff on workstations; kiosk/embedded UI",
"pros": [
"Mobile-first forces performance discipline — base CSS is what phones download; desktop gets progressive enhancement;",
"min-width queries are easier to reason about: 'at 768px AND UP, show 2 columns' beats 'at < 1024px, show 1 column';",
"Tailwind/Bootstrap defaults match this (sm/md/lg/xl/2xl) — no custom breakpoint debate;",
"Container queries (2023+) extend this to components — `.card { @container (min-width: 400px) { flex-direction: row } }`;",
"One CSS file works across 320px iPhone SE → 4K monitor — no UA sniffing, no separate m.example.com;",
"Touch targets ≥ 44×44 px (Apple HIG) become a checklist item at mobile breakpoint, not an afterthought."
],
"cons": [
"min-width cascades can hide desktop bugs — dev laptop at 1280px sees both, but a 1366px user sees overflow;",
"Tablet portrait (768px) is awkward — too narrow for desktop, too wide for phone layout;",
"Container queries need polyfill on iOS 15 (works in 16+) — feature-detect with @supports;",
"Hover-only interactions (`:hover` reveals menu) break on touch — must pair with `:focus-within` or click;",
"Bandwidth cost: base mobile CSS is small, but if you ship desktop images with `display:none` on mobile, they still download."
],
"gotchas": [
"Viewport meta tag: `<meta name=viewport content='width=device-width, initial-scale=1'>` — without it, mobile browsers render at 980px and zoom out;",
"iOS Safari 100vh = URL bar height + bottom bar — use `100dvh` (dynamic viewport) or `min-height: calc(100vh - env(safe-area-inset-bottom))`;",
"Touch target size: 44×44 px minimum (Apple) / 48×48 dp (Android) — buttons that LOOK 32px but have larger padding still fail if click area is small;",
"`<input>` font-size < 16px triggers iOS auto-zoom on focus — even on a desktop viewport in mobile Safari;",
"Landscape phone (568px height) breaks 100vh layouts — test both orientations, not just portrait;",
"Landscape iPad split-view (320px / 1024px) needs container queries, not viewport media queries;",
"Hover states on touch devices get 'sticky' (tap shows hover, doesn't dismiss until tap elsewhere) — `@media (hover: hover)` gates them;",
"Save-data header (`@media (prefers-reduced-data: reduce)`) lets users opt out of heavy assets — serve 1× images instead of 2×;",
"1px CSS ≠ 1 device pixel — retina is 3× or 4×; use `image-set()` or `srcset` for bitmap assets."
],
"anti_patterns": [
"Desktop-first (max-width) queries — reverses the cascade and adds cognitive load;",
"Custom breakpoints that don't match Tailwind/Bootstrap (sm=620 instead of 640) — engineers will guess wrong;",
"Hiding content with `display:none` instead of `loading=lazy` + responsive images — wastes bandwidth on phones;",
"Designing for one device (iPhone 14 Pro) — test on cheapest Android in your analytics top-5 countries;",
"Forgetting landscape phone — 568px tall × 100vh header + footer + content = 0 scrollable pixels."
],
"prereqs": [
"2.3"
],
"next_steps": [
"2.10"
],
"examples": [
"karaoke-studio",
"codehub-homepage",
"flowboard",
"meal-plan"
],
"snippet": ".grid { display:grid; gap:var(--space-4); grid-template-columns:1fr; } @media (min-width:640px) { .grid { grid-template-columns:repeat(2,1fr); } } @media (min-width:1024px) { .grid { grid-template-columns:repeat(4,1fr); } }",
"snippet_full": "/* Mobile-first: base = 1 column phone, scale up */\\n.grid {\\n display: grid;\\n gap: var(--space-4);\\n grid-template-columns: 1fr;\\n padding: var(--space-4);\\n}\\n@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); padding: var(--space-6); } }\\n@media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); padding: var(--space-8); } }\\n/* Touch-friendly targets */\\n.btn { min-height: 44px; min-width: 44px; padding: var(--space-3) var(--space-4); }\\n/* Hover only where hover exists */\\n@media (hover: hover) {\\n .card:hover { transform: translateY(-2px); }\\n}\\n/* Viewport meta — required */\\n/* <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1, viewport-fit=cover\\\"> */\\n.safe-top { padding-top: env(safe-area-inset-top); }\\n.safe-bottom { padding-bottom: env(safe-area-inset-bottom); }",
"tags": [
"responsive",
"mobile",
"breakpoints",
"media-queries",
"mobile-first",
"มือถือ",
"เบรกพอยต์"
],
"difficulty": "Intermediate",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries",
"compare_with": [
"Tailwind sm/md/lg/xl/2xl (utility-driven)",
"Bootstrap breakpoints (5 standard steps)",
"Container Queries (component-level, 2023+)"
]
},
{
"id": "2.8",
"title": "Dark Mode (prefers-color-scheme + Manual Toggle)",
"icon": "🌙",
"description": "Read system preference via `prefers-color-scheme`, let user override via toggle, persist choice in localStorage — three sources of truth must be reconciled in order: explicit > saved > system.",
"when_to_use": "Long-session apps (editor, dashboard, chat); users read in bed; OLED phones (battery savings); professional tool users expect it",
"when_not_to_use": "Brand-mandatory colors (banking trust colors, medical UI); print-primary deliverables; one-shot landing",
"pros": [
"OLED phones save 30–60% battery on true-black UI — measurable drain reduction;",
"Reduces eye strain in low-light reading — users stay in app 25% longer (medium-published 2019 study);",
"`prefers-color-scheme` respects OS-level accessibility settings — honors user choice automatically;",
"Manual override + localStorage = 3 layers (system, saved, explicit) — UI always shows what user picked;",
"Implementation is mostly CSS — `@media (prefers-color-scheme: dark) { :root { ... } }` works in all modern browsers;",
"Dark mode often reveals bad contrast — building it forces an a11y audit you should have done anyway."
],
"cons": [
"Every color must be defined twice (light + dark) — 2× design work, 2× QA surface, 2× screenshot tests;",
"Chart libraries (Chart.js, D3) hardcode colors — read CSS vars at chart creation, but redraw on theme change is non-trivial;",
"Images baked in light mode look wrong in dark — need `prefers-color-scheme` swap, filter inversion, or two assets;",
"Form controls have OS-specific styles — `color-scheme: dark` is needed to make selects/inputs render dark on iOS;",
"Pure #000 / pure #fff fail the 'looks right' test — true design requires off-black (#0a0a14) and off-white (#f5f5f7)."
],
"gotchas": [
"`color-scheme: dark light` on `:root` makes form controls, scrollbars, and `<input type=date>` pick up dark — without it, iOS Safari shows white inputs on dark bg;",
"Pure `#000` looks like a hole — use `hsl(240 10% 5%)` or `#0a0a14`; pure `#fff` is harsh on eyes — `#f5f5f7` is friendlier;",
"Shadows are invisible on dark — switch to borders or colored glow (`box-shadow: 0 0 0 1px rgba(255,255,255,.1)`) for elevation in dark mode;",
"Images: `<picture>` with `<source media=(prefers-color-scheme: dark) srcset=dark.png>` swaps assets; CSS `filter: invert(1) hue-rotate(180deg)` is hackier but 0 extra HTTP;",
"System preference can change at runtime (OS theme switch) — listen with `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...)`;",
"Persist explicit user choice in localStorage with key `theme`; resolve order: localStorage > system > fallback light;",
"Flash of wrong theme (FOWT) on page load — inline a tiny `<script>` in `<head>` that sets `data-theme` before CSS parses;",
"Charts using canvas need to be redrawn when theme changes — listen for theme-change custom event and call chart.update();",
"Code blocks (syntax highlighting) need a separate dark theme — Shiki/Prism both ship light + dark, switch via attribute selector."
],
"anti_patterns": [
"Auto-detect only (no manual toggle) — users on shared computers can't override; respect their preference;",
"Using pure #000 — looks like a void; use off-black for depth;",
"Two completely different palettes (light = blue, dark = orange) — defeats the purpose of theming; keep semantic roles consistent;",
"Forgetting to test form controls — iOS Safari defaults to white inputs on dark bg unless `color-scheme: dark`;",
"Persisting in sessionStorage — survives the session, vanishes on tab close; localStorage is the right tier for theme."
],
"prereqs": [
"1.2",
"2.1"
],
"next_steps": [],
"examples": [
"flowboard",
"stronghold-3d",
"codehub-homepage",
"memo-ai"
],
"snippet": "@media (prefers-color-scheme:dark) { :root { --bg:#0a0a14; --text:#f5f5f7; } } [data-theme=dark] { --bg:#0a0a14; --text:#f5f5f7; } const setTheme=t=>{document.documentElement.dataset.theme=t;localStorage.setItem('theme',t);};",
"snippet_full": "/* === Inline script in <head> to prevent flash of wrong theme === */\\n<script>\\n (function() {\\n const saved = localStorage.getItem('theme');\\n const sys = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\\n const theme = saved || sys;\\n document.documentElement.dataset.theme = theme;\\n })();\\n</script>\\n\\n/* === CSS === */\\n:root { color-scheme: light dark; --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\\n@media (prefers-color-scheme: dark) {\\n :root { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\\n}\\n[data-theme=\\\"dark\\\"] { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\\n[data-theme=\\\"light\\\"] { --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\\n\\n/* === Toggle handler === */\\nconst setTheme = t => {\\n document.documentElement.dataset.theme = t;\\n localStorage.setItem('theme', t);\\n window.dispatchEvent(new CustomEvent('themechange', { detail: t }));\\n};\\n// listen for OS theme change\\nmatchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\\n if (!localStorage.getItem('theme')) setTheme(e.matches ? 'dark' : 'light');\\n});",
"tags": [
"dark-mode",
"theme",
"prefers-color-scheme",
"oled",
"โหมดมืด",
"ธีม"
],
"difficulty": "Intermediate",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme",
"compare_with": [
"next-themes (React)",
"Theme UI (JS-driven theming)",
"Manual CSS-only (what we recommend for vanilla projects)"
]
},
{
"id": "2.9",
"title": "Glassmorphism (Frosted Glass Overlays)",
"icon": "💎",
"description": "Use `backdrop-filter: blur()` + semi-transparent background + 1px white border for premium frosted-glass panels that hint at content behind — a Material/iOS design staple.",
"when_to_use": "Overlays on busy backgrounds (3D scene, video, photo); iOS-style sheets/modals; hero sections with content underneath; premium product positioning",
"when_not_to_use": "Print-friendly pages; form-heavy input flows where contrast matters for legibility; low-end devices (Android Go, 4-year-old phones); text-heavy content where readability > style",
"pros": [
"Implies depth/hierarchy without literal shadows — UI reads as layered, modern, intentional;",
"`backdrop-filter: blur()` runs on GPU (compositor layer) — keeps main thread free for app logic;",
"Border highlight (1px rgba(255,255,255,.18)) sells the glass illusion more than blur radius does;",
"Native iOS / macOS use this exact pattern — users have trained visual literacy;",
"Content behind tells users 'there's more here' — better than opaque modals that trap focus;",
"Pairs naturally with dark mode (10% white over dark = subtle, premium feel)."
],
"cons": [
"Performance: `backdrop-filter` triggers a new compositor layer per element — 20 glass panels = 20 layers, GPU memory spikes;",
"Low-end Android (pre-2020 Mali GPUs) can drop to 5–10 fps when 5+ glass elements are visible;",
"Contrast nightmare: white-on-light-photo text disappears; always pair with text-shadow or text on a separate dark band;",
"Safari needs `-webkit-backdrop-filter` AND modern syntax — older Safari (pre-18) renders wrong;",
"Print styles: `backdrop-filter` is invisible in PDF/print — explicit fallback bg color required for `@media print`."
],
"gotchas": [
"Always declare `-webkit-backdrop-filter` before `backdrop-filter` — Safari < 18 ignores unprefixed; `-moz` was never needed;",
"Blur radius sweet spot is 8–16px; > 24px looks fake (gaussian stack overflows) and tanks perf;",
"Background opacity must be 5–15% on dark bg, 40–60% on light bg — too transparent = invisible, too opaque = not glass;",
"Add `border: 1px solid rgba(255,255,255,.18)` — without it, glass edge looks ragged against varying backgrounds;",
"iOS notch + safe-area: glass sheet must include `padding-bottom: env(safe-area-inset-bottom)` or home indicator overlaps;",
"`backdrop-filter` doesn't blur elements ABOVE the glass — only those below in stacking order;",
"Detect low-end devices: `@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; background: var(--color-surface); } }` (Safari);",
"Inside a transformed parent (`transform: translateZ(0)`), backdrop-filter can fail entirely — promote to its own layer;"
],
"anti_patterns": [
"Glass over glass (nested panels) — multiple compositor layers stack and slow to single-digit fps;",
"Glass containing dense text without a contrast layer — a11y fails on WCAG 1.4.3 (4.5:1);",
"Blur radius 40+ — looks like an Instagram filter, not a design choice;",
"Mixing glass with flat opaque panels in same UI — visual inconsistency, looks accidental;",
"Applying glass to entire body bg — wipes GPU memory on first paint, scroll stutters for 3 seconds."
],
"prereqs": [
"2.4"
],
"next_steps": [],
"examples": [
"stronghold-3d",
"karaoke-studio",
"sj88-video-editor-pro",
"eventtix"
],
"snippet": ".glass { background:rgba(255,255,255,.1); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,.2); }",
"snippet_full": ".glass {\\n background: rgba(255, 255, 255, 0.08);\\n backdrop-filter: blur(12px) saturate(180%);\\n -webkit-backdrop-filter: blur(12px) saturate(180%);\\n border: 1px solid rgba(255, 255, 255, 0.18);\\n border-radius: var(--radius-lg);\\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\\n}\\n.glass-dark {\\n background: rgba(10, 10, 20, 0.55);\\n color: hsl(0 0% 96%);\\n}\\n/* Low-end / accessibility fallback */\\n@media (prefers-reduced-transparency: reduce) {\\n .glass {\\n backdrop-filter: none;\\n -webkit-backdrop-filter: none;\\n background: var(--color-surface);\\n }\\n}\\n/* Safe-area aware sheet */\\n.sheet {\\n padding: var(--space-6);\\n padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));\\n}",
"tags": [
"glass",
"glassmorphism",
"blur",
"backdrop-filter",
"frosted",
"กระจกฝ้า"
],
"difficulty": "Beginner",
"time_to_learn": "20 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter",
"compare_with": [
"Solid card with shadow (cheaper, more accessible)",
"Frosted image (manual gaussian via filter:blur)",
"Native iOS UIVisualEffectView (Apple-only)"
]
},
{
"id": "2.10",
"title": "Layout Primitives (Grid, Flex, Stack, Cluster)",
"icon": "📐",
"description": "Compose layouts from 4 named primitives — Grid (2D), Flex (1D), Stack (vertical w/ consistent gap), Cluster (horizontal wrap) — instead of inventing per-component magic numbers.",
"when_to_use": "Every layout; any time you're tempted to write `position: absolute` to align two things; you want RTL support for free",
"when_not_to_use": "Single absolutely-positioned element (1-off); canvas/WebGL scenes where layout is in world-space; print stylesheet",
"pros": [
"Grid for 2D, Flex for 1D — picking the right one upfront saves hours of `justify-content: center` guessing;",
"`grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` is responsive without a single media query — 1 to N columns automatically;",
"Stack primitive (`> * + * { margin-top: var(--gap) }`) gives vertical rhythm with zero magic numbers;",
"Container queries (`@container`) make components responsive to their parent, not viewport — cards in sidebars vs main column both work;",
"Grid + flex are RTL-ready by default (`direction: rtl` flips everything) — no `transform: scaleX(-1)` hacks;",
"aspect-ratio (2021+) eliminates padding-bottom hack for 16:9 / 1:1 / 4:3 containers — `aspect-ratio: 16/9;` and done."
],
"cons": [
"Grid + flex syntax has a learning curve — `place-items: center` vs `justify-items + align-items` confuses new devs;",
"Container queries (2023+) need fallback for older Safari — `@supports (container-type: inline-size)` gates the rule;",
"Subgrid (Firefox-first, partial Chrome/Safari 2024+) isn't universal — nested grids can't share tracks reliably yet;",
"Stack via `> * + * { margin-top }` doesn't work with pseudo-elements that already have margin — manual override needed;",
"IE 11 doesn't support grid at all — irrelevant for modern apps, but legacy enterprise clients will break."
],
"gotchas": [
"`grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` collapses empty tracks — use `auto-fill` if you want fixed columns even when empty;",
"Flex `gap` (2021+) replaces margin-between-children — but `gap` on flexbox needs Chromium 84+, Safari 14.1+;",
"`place-items: center` is shorthand for `align-items + justify-items` — saves 14 chars, reads better;",
"Container queries need `container-type: inline-size` on parent + `@container (min-width: 400px)` on child; without the type declaration, nothing works;",
"Stack via `> * + * { margin-top }` only applies between adjacent children — pseudo-elements like `::before` still get the margin if they exist;",
"`min-height: 100dvh` (dynamic viewport) handles mobile URL-bar collapse better than `100vh` — supported in iOS Safari 15.4+;",
"`aspect-ratio` overrides `width`/`height` if both are set — set width:100% and aspect-ratio, not width+height;",
"Negative margins on grid items can leak outside the container — use `overflow: hidden` on parent or grid gaps > 0;",
"`place-content: center` centers the entire grid within its container (different from `place-items` which centers items within their cells)."
],
"anti_patterns": [
"Float-based layouts (legacy `float: left`) — never; flex/grid subsume every float use case;",
"`position: absolute` for centering — flex `place-items: center` is 1 line and respects content size;",
"Nested flex inside flex inside flex (> 4 deep) — refactor to grid or extract a primitive;",
"Magic numbers (`margin-top: 17px` 'to make it line up') — every magic number is a future bug;",
"Setting both `width` and `height` on an image inside flex — use `object-fit: cover` and let aspect-ratio handle the container."
],
"prereqs": [
"2.3"
],
"next_steps": [],
"examples": [
"codehub-homepage",
"flowboard",
"knowledge-garden",
"eventtix"
],
"snippet": ".grid { display:grid; gap:var(--space-4); grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); } .stack > * + * { margin-top: var(--space-4); }",
"snippet_full": "/* === Grid: 2D responsive layout, no media queries === */\\n.grid {\\n display: grid;\\n gap: var(--space-4);\\n grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\\n}\\n.grid-3 { grid-template-columns: repeat(3, 1fr); }\\n\\n/* === Flex: 1D horizontal/vertical alignment === */\\n.row { display: flex; gap: var(--space-4); align-items: center; }\\n.row-end { display: flex; gap: var(--space-4); justify-content: flex-end; }\\n\\n/* === Stack: vertical rhythm with consistent gap === */\\n.stack > * + * { margin-top: var(--space-4); }\\n.stack-sm > * + * { margin-top: var(--space-2); }\\n.stack-lg > * + * { margin-top: var(--space-8); }\\n\\n/* === Cluster: wrap-on-overflow horizontal === */\\n.cluster {\\n display: flex; flex-wrap: wrap; gap: var(--space-3);\\n align-items: center; justify-content: flex-start;\\n}\\n\\n/* === Aspect-ratio: 16:9 video, 1:1 avatar, 4:3 card === */\\n.media-16x9 { aspect-ratio: 16 / 9; width: 100%; object-fit: cover; }\\n.avatar { aspect-ratio: 1; border-radius: 50%; }\\n\\n/* === Container queries: component-responsive, not viewport === */\\n.card-container { container-type: inline-size; }\\n@container (min-width: 400px) {\\n .card { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); }\\n}",
"tags": [
"layout",
"grid",
"flex",
"container-queries",
"stack",
"เลย์เอาต์"
],
"difficulty": "Beginner",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout",
"compare_with": [
"Flexbox (1D only)",
"CSS Grid (2D, what we use)",
"Container Queries (component-responsive)",
"Subgrid (Firefox-first, partial)",
"CSS `display: contents` (deprecated, accessibility risk)"
]
}
]
}