{ "$schema": "../schemas/code-crispies-module-schema.json", "id": "units-variables", "title": "CSS Units & Variables", "description": "Understand the variety of CSS measurement units and how to define and use custom properties for maintainable styles.", "difficulty": "beginner", "lessons": [ { "id": "units-1", "title": "Absolute vs. Relative Units", "description": "Learn the difference between px, rem, em, %, and vw/vh for flexible, responsive layouts.

width: 80%;      /* relative to parent */\nmax-width: 40rem; /* relative to root font */\npadding: 16px;   /* fixed pixels */
", "task": "Set the width of .box to 80% and max-width to 40rem.", "previewHTML": "
Resize me!
", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .box { background: #f5f5f5; padding: 1rem; }", "sandboxCSS": "", "codePrefix": "/* Set flexible sizing */\n.box {", "initialCode": "", "codeSuffix": "}", "solution": " width: 80%;\n max-width: 40rem;", "previewContainer": "preview-area", "concept": { "explanation": "CSS units fall into two categories: absolute units (px) with fixed sizes, and relative units (%, rem, em, vw, vh) that scale based on context. Absolute units like 16px always render at the same physical size regardless of user settings, while relative units adapt to user preferences and screen sizes. Rem (root em) is preferred for most spacing because 1rem equals the root font size (usually 16px), so if a user increases their browser's font size for accessibility, all rem-based spacing scales proportionally. Percentage units (%) are relative to the parent element's size, making them perfect for fluid layouts that adapt to container width.", "diagram": "Unit Types & How They Calculate\n\nAbsolute Units (Fixed Size):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\npx (pixels) → 16px always = 16 pixels\n (ignores user font settings)\n\nRelative Units (Scale with Context):\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nrem (root em) → 1rem = root font-size\n If html { font-size: 16px }\n then 2rem = 32px\n Scales with user settings ✓\n\n% (percent) → 80% = 80% of parent width\n Parent: 500px → 80% = 400px\n Parent: 800px → 80% = 640px\n Fluid layouts ✓\n\nem (element) → 1em = current font-size\n .box { font-size: 20px }\n padding: 1em = 20px\n Compounds in nested elements ⚠️\n\nvw/vh → 50vw = 50% viewport width\n 1vh = 1% viewport height\n\nWhy rem is preferred:\n┌──────────────────────────────────┐\n│ User increases browser font size │\n│ (Settings → Appearance → Text) │\n└────────────┬─────────────────────┘\n ↓\n┌──────────────────────────────────┐\n│ rem-based spacing scales up ✓ │\n│ px-based spacing stays fixed ✗ │\n└──────────────────────────────────┘\nAccessibility & responsive design!" }, "validations": [ { "type": "contains", "value": "width", "message": "Use width property", "options": { "caseSensitive": false } }, { "type": "property_value", "value": { "property": "width", "expected": "80%" }, "message": "Set width to 80%" }, { "type": "contains", "value": "max-width", "message": "Use max-width property", "options": { "caseSensitive": false } }, { "type": "property_value", "value": { "property": "max-width", "expected": "40rem" }, "message": "Set max-width to 40rem" } ] }, { "id": "units-2", "title": "CSS Custom Properties", "description": "Define and reuse variables (--custom properties) to centralize your theme values.

:root {\n  --main-color: mediumpurple;\n}\n.themed {\n  border-color: var(--main-color);\n}
", "task": "Create a --main-color variable in :root with mediumpurple and apply it as the border color on .themed.", "previewHTML": "
Variable Box
", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .themed { padding: 1rem; border: 0.125rem solid #ddd; }", "sandboxCSS": "", "codePrefix": "/* Define and use a CSS variable */\n:root {", "initialCode": "", "codeSuffix": "}\n.themed { }", "solution": " --main-color: mediumpurple;\n}\n.themed {\n border-color: var(--main-color);", "previewContainer": "preview-area", "concept": { "explanation": "CSS custom properties (variables) are values you define once and reference throughout your stylesheet using the var() function. They follow the CSS cascade, meaning variables defined on :root (the html element) are inherited by all descendants, while variables defined on specific elements are scoped to those elements and their children. When you change a custom property value, all references to that variable automatically update, making theme management and design systems much easier to maintain. Unlike preprocessor variables (Sass/Less), CSS custom properties are live in the browser and can be updated dynamically with JavaScript or media queries.", "diagram": "CSS Custom Properties & Inheritance\n\nDefinition & Reference:\n\n:root {\n --main-color: mediumpurple; ← Define\n --spacing: 1rem;\n}\n\n.themed {\n border-color: var(--main-color); ← Reference\n padding: var(--spacing);\n}\n\nInheritance cascade:\n\n┌─────────────────────────────┐\n│ :root (html element) │\n│ --primary: blue │ ← Defined here\n└──────────┬──────────────────┘\n │ Inherits ↓\n ┌──────┴──────┐\n │ body │\n │ (inherits) │\n └──────┬──────┘\n │ Inherits ↓\n ┌──────┴──────────┐\n │ .card │\n │ color: var(--primary) │ ← Can use it!\n └─────────────────┘\n\nScoping example:\n\n:root { --theme: light; }\n\n.dark-mode {\n --theme: dark; ← Overrides for this\n} subtree only\n\nDynamic updates:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nCSS: :root { --size: 16px; }\n ↓\nJS: document.documentElement\n .style.setProperty('--size', '20px');\n ↓\nAll var(--size) references update! ✓\n\nVs Preprocessor Variables:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSass: $color: blue; ← Compile-time\nCSS: --color: blue; ← Runtime (live)" }, "validations": [ { "type": "contains", "value": "--main-color", "message": "Define --main-color in :root", "options": { "caseSensitive": false } }, { "type": "contains", "value": "var(--main-color)", "message": "Use var(--main-color)", "options": { "caseSensitive": false } }, { "type": "property_value", "value": { "property": "border", "expected": "var(--main-color)" }, "message": "Apply variable to border color", "options": { "exact": false } } ] }, { "id": "units-3", "title": "Unit Calculations (calc)", "description": "Use the calc() function to combine different units in one expression.

width: calc(100% - 2rem);\nmin-height: calc(10vh + 1rem);
", "task": "Set the width of .sized to calc(100% - 2rem) and min-height to calc(10vh + 1rem).", "previewHTML": "
Calc Demo
", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .sized { background: #e8f5e9; padding: 1rem; }", "sandboxCSS": "", "codePrefix": "/* Use calc for dynamic sizing */\n.sized {", "initialCode": "", "codeSuffix": "}", "solution": " width: calc(100% - 2rem);\n min-height: calc(10vh + 1rem);", "previewContainer": "preview-area", "concept": { "explanation": "The calc() function performs mathematical calculations at runtime, allowing you to mix different unit types (px, %, rem, vw, etc.) in a single expression. The browser evaluates calc() expressions during layout calculation, after all relative units have been resolved to their pixel values, then performs the arithmetic operation. This is powerful for responsive layouts because you can combine fluid units (%) with fixed spacing (rem/px), like calc(100% - 2rem) for \"full width minus some padding.\" Spaces around + and - operators are required because calc(10vh+1rem) would be parsed as a single invalid unit, while calc(10vh + 1rem) correctly separates the operands.", "diagram": "How calc() Works at Runtime\n\nExpression: width: calc(100% - 2rem);\n\nStep 1: Resolve relative units\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nParent width: 500px\n100% → 500px\n\nRoot font-size: 16px\n2rem → 32px (2 × 16)\n\nStep 2: Perform calculation\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(100% - 2rem)\n → calc(500px - 32px)\n → 468px ✓\n\nVisual representation:\n\n┌──────────────────────────────┐\n│ Parent container (500px) │\n│ ┌──────────────────────────┐ │\n│ │ .sized (468px) │ │ ← calc(100% - 2rem)\n│ │ │ │\n│ └──────────────────────────┘ │\n│ ◀── 16px gap (1rem) on each │\n│ side = 32px total │\n└──────────────────────────────┘\n\nMixing units:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(100% - 2rem) Fluid - Fixed\ncalc(50vw + 100px) Viewport + Fixed\ncalc(2rem * 3) Multiplication\ncalc(100% / 3) Division\n\nImportant syntax rules:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\ncalc(10vh + 1rem) ✓ Spaces around +/-\ncalc(10vh+1rem) ✗ Treated as one unit\ncalc(2rem * 3) ✓ No space needed for */\ncalc(100%/3) ✓ Division works too" }, "validations": [ { "type": "contains", "value": "calc", "message": "Use calc() function", "options": { "caseSensitive": false } }, { "type": "regex", "value": "width:\\s*calc\\(100% - 2rem\\)", "message": "Width should be calc(100% - 2rem)", "options": { "caseSensitive": false } }, { "type": "regex", "value": "min-height:\\s*calc\\(10vh \\+ 1rem\\)", "message": "Min-height should be calc(10vh + 1rem)", "options": { "caseSensitive": false } } ] }, { "id": "units-4", "title": "Viewport & Responsive Units", "description": "Control layouts relative to viewport size with vw, vh, and vmin/vmax units.

width: 50vw;   /* 50% of viewport width */\nheight: 20vh;  /* 20% of viewport height */
", "task": "Give .view a width of 50vw and height of 20vh.", "previewHTML": "
Viewport Box
", "previewBaseCSS": "body { font-family: sans-serif; padding: 1rem; } .view { background: #ffe0b2; }", "sandboxCSS": "", "codePrefix": "/* Use viewport units */\n.view {", "initialCode": "", "codeSuffix": "}", "solution": " width: 50vw;\n height: 20vh;", "previewContainer": "preview-area", "concept": { "explanation": "Viewport units (vw, vh, vmin, vmax) are relative to the browser window dimensions, not the parent element. 1vw equals 1% of the viewport width, and 1vh equals 1% of the viewport height, so 50vw is always half the screen width regardless of element nesting. These units are perfect for full-screen hero sections, responsive typography, and layouts that scale with screen size. The browser recalculates viewport unit values when the window is resized, making elements automatically adapt without media queries. Vmin uses the smaller dimension (min of width/height) while vmax uses the larger, useful for ensuring elements fit on both portrait and landscape orientations.", "diagram": "Viewport Units & How They Calculate\n\nViewport Dimensions:\n┌────────────────────────────────┐ ↕\n│ │ 800px\n│ Browser Window │ viewport\n│ (Viewport) │ height\n│ │ ↕\n│ │\n└────────────────────────────────┘\n◀────── 1400px viewport width ──▶\n\nUnit calculations:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n1vw = 1400px ÷ 100 = 14px\n1vh = 800px ÷ 100 = 8px\n\n50vw = 50 × 14px = 700px (half width)\n20vh = 20 × 8px = 160px (1/5 height)\n\nvmin = min(1vw, 1vh) = min(14px, 8px) = 8px\nvmax = max(1vw, 1vh) = max(14px, 8px) = 14px\n\nViewport vs Percentage:\n\n% units (relative to parent):\n┌─────────────────────────────┐\n│ Parent (600px) │\n│ ┌─────────────────────┐ │\n│ │ width: 50% │ │ ← 300px\n│ │ (50% of parent) │ │ (50% of 600px)\n│ └─────────────────────┘ │\n└─────────────────────────────┘\n\nvw units (relative to viewport):\n┌────────────────────────────────┐ Viewport (1400px)\n│ ┌──────────────────────┐ │\n│ │ Parent (600px) │ │\n│ │ ┌──────────────────┐ │ │\n│ │ │ width: 50vw │ │ │ ← 700px\n│ │ │ (50% of viewport)│ │ │ (50% of 1400px)\n│ │ └──────────────────┘ │ │ Escapes parent!\n│ └──────────────────────┘ │\n└────────────────────────────────┘\n\nCommon use cases:\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFull-screen: width: 100vw; height: 100vh;\nHero section: min-height: 80vh;\nResponsive: font-size: calc(1rem + 1vw);\nSquare ratio: width: 50vmin; height: 50vmin;" }, "validations": [ { "type": "contains", "value": "vw", "message": "Use vw unit", "options": { "caseSensitive": false } }, { "type": "contains", "value": "vh", "message": "Use vh unit", "options": { "caseSensitive": false } }, { "type": "property_value", "value": { "property": "width", "expected": "50vw" }, "message": "Set width to 50vw" }, { "type": "property_value", "value": { "property": "height", "expected": "20vh" }, "message": "Set height to 20vh" } ] } ] }