:target Modal

This modal is opened purely via a URL hash (href="#target-modal") and the CSS :target pseudo-class. Zero JavaScript — it's native CSS!

The browser URL changes to #target-modal when open.

✕ Close Modal
CSS Color, Design Systems & Interactive Components

Pick Better
Colors. Build
Smarter CSS.

Learn color theory the CSS way — from the color wheel to oklch(). Build a complete design system with semantic tokens, spacing, and shadows. Then construct 10 fully interactive website components with zero JavaScript: dropdowns, modals, tabs, accordions, and more.

Color Theory Design Tokens CSS-Only UI Dark Mode ● Zero JavaScript

§ 01 — Color Relationships

Color Wheel & Harmony

Every great color palette comes from understanding the color wheel. Pick a base hue, then derive harmonious partners using one of five relationships. The interactive wheel below shows them all.

LIVE — Spin the color wheel
color-harmony.css
/* Color Harmony in CSS HSL:
   Base hue = H. Derive partners by
   rotating the hue value. */

/* Complementary — opposite on wheel */
--base: hsl(265, 80%, 60%);  /* violet */
--comp: hsl(85,  70%, 50%);  /* 265+180=85° lime */

/* Analogous — 30° neighbors */
--ana1: hsl(235, 80%, 60%);
--ana2: hsl(295, 80%, 60%);

/* Triadic — 120° apart */
--tri1: hsl(25,  80%, 60%);  /* 265-240 */
--tri2: hsl(145, 70%, 45%);  /* 265-120 */

/* Split-Complementary */
--spl1: hsl(55,  80%, 55%);  /* 265+150 */
--spl2: hsl(115, 70%, 50%);  /* 265+210 */
Rule of thumb: Complementary colors create maximum contrast and energy. Analogous colors feel calm and cohesive. Triadic is vibrant but balanced. Start with one brand color and use these formulas to build the rest.

§ 02 — Hue · Saturation · Lightness

HSL Deep Dive

HSL is the most intuitive color format for web design. Unlike hex or RGB which describe amounts of light, HSL describes the color itself — making it trivial to lighten, darken, desaturate, or shift hue programmatically.

INTERACTIVE — Live HSL Explorer
hsl(265, 80%, 60%)
hsl-tricks.css
/* HSL: hsl(H, S%, L%)
   H = 0-360° position on color wheel
   S = 0-100% gray → full color
   L = 0-100% black → white (50% = pure color) */

--brand:          hsl(265, 80%, 60%);

/* Tints (lighter) — increase L */
--brand-100:      hsl(265, 80%, 95%);
--brand-200:      hsl(265, 80%, 85%);

/* Shades (darker) — decrease L */
--brand-700:      hsl(265, 80%, 30%);
--brand-900:      hsl(265, 80%, 10%);

/* Muted — decrease S */
--brand-muted:    hsl(265, 20%, 60%);

/* With alpha — use hsl(...) + / */
--brand-ghost:    hsl(265 80% 60% / 0.15);

/* Why HSL over hex?
   hex #9b59ff  — no idea what this means
   hsl(265,80%,60%) — violet, vivid, medium   */

§ 03 — From one color to a full system

Building a Palette

A professional palette has one brand/primary color, an accent, a neutral scale (gray), semantic colors (success, warning, danger), and surface colors for backgrounds. All derived from one starting hue.

LIVE — Palette Generator
265°
design-palette.css
/* Complete palette derived from one hue (265°) */
:root {
  /* Brand scale (50-900) */
  --brand-50:   hsl(265 80% 97%);
  --brand-100:  hsl(265 80% 93%);
  --brand-400:  hsl(265 80% 72%);
  --brand-500:  hsl(265 80% 60%);  /* PRIMARY */
  --brand-700:  hsl(265 80% 35%);
  --brand-900:  hsl(265 80% 12%);

  /* Neutral scale (same hue, low saturation) */
  --neutral-50: hsl(265 8%  97%);
  --neutral-500:hsl(265 8%  56%);
  --neutral-900:hsl(265 8%  8% );

  /* Semantic (fixed hues, match saturation) */
  --success: hsl(135 70% 42%);
  --warning: hsl(40  95% 52%);
  --danger:  hsl(350 80% 58%);
  --info:    hsl(198 80% 52%);
}

§ 04 — WCAG 2.1 Contrast Ratios

Contrast & Accessibility

WCAG requires a minimum contrast ratio of 4.5:1 for normal text (AA) and 7:1 for the AAA level. Large text (18px+ bold) only needs 3:1. The contrast ratio of 21:1 is pure black on white.

LIVE — WCAG Contrast Checker

Sample Heading Text

The quick brown fox jumps over the lazy dog. This paragraph shows body text at normal size.

—:1
contrast-rules.css
/* WCAG 2.1 Requirements
   ──────────────────────────────────────────
   AA Normal text   ≥ 4.5:1
   AA Large text    ≥ 3:1  (18px+ or 14px+ bold)
   AAA Normal text  ≥ 7:1
   AAA Large text   ≥ 4.5:1
   UI Components    ≥ 3:1  (borders, icons)

   Common Mistakes:
   ❌ Gray text on white: #888 on white = 3.5:1 FAIL
   ❌ Light on light: #ffd700 on #fff = 1.1:1 FAIL
   ❌ Low-sat color on bg: #9b59ff on #7030d0 = FAIL
   ✓  #f0eafa on #0a0814 = 16.8:1 PASS AAA
   ✓  #ff6b2b on #0a0814 = 7.2:1  PASS AAA  */

/* Test with browser DevTools:
   Inspect element → Accessibility → Contrast  */

/* Contrast-safe text-on-background patterns: */
.on-dark  { color: hsl(0 0% 95%); background: hsl(0 0% 8%); }
.on-light { color: hsl(0 0% 10%); background: hsl(0 0% 98%); }
.muted    { color: hsl(265 20% 65%); } /* ≥4.5:1 on dark */

§ 05 — Modern CSS Color Functions

oklch() & color-mix()

oklch() is perceptually uniform — changing Lightness always looks equally bright, unlike HSL. color-mix() blends two colors in any space. Together, they make dynamic tinting trivial.

LIVE — oklch Gradient vs HSL Gradient
HSL gradient (notice the muddy middle):
oklch gradient (smooth, no muddiness):
color-mix() — tint/shade your brand color:
oklch-colormix.css
/* oklch(L C H [/ alpha])
   L = 0-1 lightness (perceptual)
   C = 0-0.4 chroma (saturation)
   H = 0-360° hue               */

--violet: oklch(0.55 0.22 293);
--orange: oklch(0.65 0.2  38 );
--cyan:   oklch(0.78 0.12 192);

/* Adjust only lightness — same feel */
--violet-light: oklch(0.85 0.22 293);
--violet-dark:  oklch(0.30 0.22 293);

/* color-mix(in space, color1 %, color2) */
--tinted: color-mix(in oklch, var(--violet) 25%, white);
--shaded: color-mix(in oklch, var(--violet) 75%, black);
--muted:  color-mix(in oklch, var(--violet) 60%, gray);

/* Relative color syntax (CSS 4) — modify one channel */
--darker:  oklch(from var(--violet) calc(l - 0.2) c h);
--warmer:  oklch(from var(--violet) l c calc(h + 30));
--faded:   oklch(from var(--violet) l calc(c * 0.3) h);

§ 06 — Design Token Architecture

Semantic Color Tokens

Don't reference raw color values in your components. Use semantic tokens — names that describe purpose, not appearance. Then you can retheme the whole site by changing only the token definitions.

DESIGN TOKEN SYSTEM
TokenPurposeDark ValueLight Value
--color-primaryMain brand action
#ff6b2b
#d44000
--color-surfaceCard backgrounds
#18152a
#e8e2f8
--color-on-surfaceText on surface
#f0eafa
#160e2e
--color-borderDividers, outlines
#241f3a
#ccc5e8
--color-successConfirmed, done
#7ddf64
#2a8a00
--color-dangerErrors, delete
#ff4d7e
#b8002e
--color-warningAlerts, caution
#ffc94d
#8a5200
--color-mutedPlaceholder text
#4a3f72
#9488bb
design-tokens.css
:root {
  /* ── PRIMITIVE TOKENS (raw values) ─────────── */
  --violet-500: hsl(265 80% 60%);
  --orange-500: hsl(20  100% 59%);
  --neutral-50: hsl(265 8%  97%);
  --neutral-900:hsl(265 8%  8% );
}

:root[data-theme="dark"] {
  /* ── SEMANTIC TOKENS (reference primitive) ── */
  --color-primary:    var(--orange-500);
  --color-bg:         hsl(265 20% 5%);
  --color-surface:    hsl(265 15% 10%);
  --color-on-surface: var(--neutral-50);
  --color-muted:      hsl(265 15% 50%);
  --color-border:     hsl(265 15% 20%);
}

:root[data-theme="light"] {
  --color-primary:    hsl(20 100% 42%); /* darker on light */
  --color-bg:         var(--neutral-50);
  --color-surface:    hsl(265 15% 93%);
  --color-on-surface: var(--neutral-900);
  --color-muted:      hsl(265 15% 55%);
  --color-border:     hsl(265 15% 82%);
}

§ 07 — prefers-color-scheme + manual toggle

Dark / Light Mode

This page itself supports light mode — click the toggle in the sidebar. The right approach combines prefers-color-scheme media query (respects OS preference) with a manual override via a data-theme attribute on <html>.

Dark Mode

Easier on eyes at night. Choose dark surfaces with high-contrast text. Reduce pure black — dark gray (#111) feels more premium than #000. Colored accents need to be bright (high lightness) to pop against dark backgrounds.

Light Mode

Cleaner for reading-heavy content. Use off-white (#faf8ff) not pure white. Your brand color may need to darken (reduce lightness) to pass contrast on light backgrounds. Shadows become more visible and important.

dark-light-mode.css
/* METHOD 1 — OS preference auto (no JS) */
@media (prefers-color-scheme: dark) {
  :root { --bg: hsl(265 20% 5%); --text: hsl(265 50% 95%); }
}
@media (prefers-color-scheme: light) {
  :root { --bg: hsl(265 20% 97%); --text: hsl(265 20% 8%); }
}

/* METHOD 2 — Manual toggle with data-theme */
html[data-theme="dark"]  { --bg: #0a0814; --text: #f0eafa; }
html[data-theme="light"] { --bg: #faf8ff; --text: #160e2e; }

// METHOD 2 — JS toggle
const current = html.dataset.theme;
html.dataset.theme = current === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', html.dataset.theme);

/* METHOD 3 — CSS-only toggle with checkbox (:has) */
html:has(#light-toggle:checked) {
  --bg: #faf8ff; --text: #160e2e;
}

/* Always smooth transitions */
* { transition: background-color .3s, color .3s, border-color .3s; }

§ 08 — Type Scale System

Typography Scale

A type scale uses a ratio to create visual hierarchy. The Major Third (1.25×) is subtle. The Perfect Fifth (1.5×) is dramatic. Use clamp() for fluid type that scales between min and max with the viewport.

LIVE — Type Scale Explorer
16px
typography-scale.css
/* Major Third scale (1.25×), base 16px */
:root {
  --text-xs:   clamp(0.64rem,  1.2vw, 0.75rem);  /* ~10–12px */
  --text-sm:   clamp(0.75rem,  1.4vw, 0.875rem); /* ~12–14px */
  --text-base: clamp(1rem,     1.6vw, 1.125rem); /* ~16–18px */
  --text-lg:   clamp(1.25rem,  2vw,   1.5rem);   /* ~20–24px */
  --text-xl:   clamp(1.563rem, 2.5vw, 2rem);    /* ~25–32px */
  --text-2xl:  clamp(1.953rem, 3vw,   2.5rem);  /* ~31–40px */
  --text-3xl:  clamp(2.441rem, 4vw,   3.5rem);  /* ~39–56px */
  --text-4xl:  clamp(3.052rem, 5vw,   5rem);   /* ~49–80px */

  /* Line heights */
  --leading-tight: 1.15;  /* headlines */
  --leading-snug:  1.4;   /* subheadings */
  --leading-normal:1.7;   /* body text */
  --leading-loose: 2.0;   /* captions */
}

§ 09 — 4px / 8px Grid

Spacing System

All spacing — padding, margin, gap, width — should be multiples of 4px (or an 8px base). This creates visual rhythm. Define a spacing scale as CSS variables so you never type a raw pixel value again.

LIVE — 4px Spacing Scale
spacing-system.css
:root {
  --space-1:   4px;    /* tight gap, icon padding */
  --space-2:   8px;    /* small gaps */
  --space-3:   12px;   /* button padding */
  --space-4:   16px;   /* card padding (base) */
  --space-5:   20px;
  --space-6:   24px;   /* section padding */
  --space-8:   32px;
  --space-10:  40px;
  --space-12:  48px;
  --space-16:  64px;   /* section separator */
  --space-24:  96px;   /* hero sections */

  /* Container widths */
  --container-sm:  640px;
  --container-md:  768px;
  --container-lg:  1024px;
  --container-xl:  1280px;
}

/* Use tokens everywhere */
.card   { padding: var(--space-6); }
.btn    { padding: var(--space-3) var(--space-5); }
.layout { gap: var(--space-8); }
.page   { max-width: var(--container-lg); margin: 0 auto; }

§ 10 — Shadow Depth System

Elevation & Shadows

Shadows convey depth — a consistent elevation system makes your UI feel physically coherent. Use multiple shadow layers for realism, and match shadow color to your background hue, not pure black.

VISUAL — 5 Elevation Levels (hover each card)
Flat
e0
Low
e1
Medium
e2
High
e3
Modal
e4
shadow-system.css
:root {
  /* Shadow hue matches background (265°) */
  --shadow-color: 265 30% 5%;

  --shadow-1: 0 1px 3px   hsl(var(--shadow-color) / 0.30);
  --shadow-2: 0 4px 12px  hsl(var(--shadow-color) / 0.35);
  --shadow-3: 0 8px 24px  hsl(var(--shadow-color) / 0.40),
              0 2px 6px   hsl(var(--shadow-color) / 0.25);
  --shadow-4: 0 16px 40px hsl(var(--shadow-color) / 0.45),
              0 4px 12px  hsl(var(--shadow-color) / 0.30);

  /* Colored glow shadows */
  --glow-orange: 0 0 20px  hsl(20 100% 59% / 0.35);
  --glow-violet: 0 0 24px  hsl(265 80% 60% / 0.3);

  /* Inner shadow */
  --inner-shadow: inset 0 1px 3px rgba(0,0,0,.3);
}

§ 11 — Zero-JS Interactive Forms

Custom Form Controls in Pure CSS

CSS can completely re-style native form elements. Hide the native control, place a styled replacement right after it in the HTML, and use the adjacent sibling selector (input:checked + .custom) to reflect state.

Custom Checkboxes
Custom Radio
Toggle Switches
Styled Range Slider

§ 12 — Hover-triggered dropdowns

CSS-Only Navigation

No JavaScript needed for a professional dropdown nav. Use position:relative on the nav item, position:absolute on the dropdown, and :hover to reveal it with opacity + transform transitions.

LIVE — Hover the menu items

Zero JavaScript — hover Products or Docs for dropdown

§ 13 — :target and :has() modal techniques

CSS-Only Modals

Two techniques — the :target hack uses URL hash navigation (the modal overlay shows when its ID matches the URL hash), and the :has() technique uses a hidden checkbox to toggle visibility.

LIVE — Open these modals with ZERO JavaScript

:has() Modal

This modal is controlled by a hidden checkbox. When the checkbox is checked, the overlay becomes visible. The :has() selector watches for the checked state. Still zero JavaScript!

§ 14 — Radio input + sibling selectors

CSS-Only Tabs

The radio button trick: hidden radio inputs track the active tab. The :checked state cascades via sibling selectors to show the right panel. The entire tab component — no JavaScript.

LIVE — Click tabs (pure CSS radio inputs)

Overview

This is the overview panel. It's shown because the Tab 1 radio input is checked. When you click another tab label, it checks a different radio, which triggers CSS sibling selectors to hide this panel and show another.

Specifications

Specifications content here. This whole tab system is driven by CSS — the #tab2:checked ~ .tab2-content selector makes this visible when Tab 2 is active.

Reviews ★★★★★

"Absolutely brilliant technique — I can't believe this works without JavaScript!" This panel is hidden until Tab 3's radio is checked.

§ 15 — CSS Hover Interactions

Hover Effects & Cards

Compelling hover effects need only CSS — GPU-composited transform and opacity changes are buttery smooth. The mouse-tracking glow uses a CSS custom property set by JavaScript on mousemove.

LIVE — Hover each card style
Flip Card
Hover to
flip! 🎴
Flipped!
Pure CSS ✨
Mouse-Tracking Glow

Radial Glow Card

Move your mouse around inside this card to see the glow follow your cursor. It uses a CSS custom property updated by a single mousemove listener.

Reveal Overlay
🎨
Hover to reveal
Fill Button
Hover for fill effect

§ 16 — Complete Button System

Button Design System

A complete button system covers variant (solid, outlined, ghost), semantic role (primary, danger, success), size (sm → xl), state (loading, disabled, icon-only), and hover micro-interactions.

LIVE — All Button Variants (hover each)
Solid
Outlined
Sizes
States
With icons

§ 17 — Visual Treatment Techniques

Glass, Grain & Glow

Glassmorphism — frosted glass with backdrop-filter:blur(). Grain — SVG noise as a pseudo-element adds depth. Neon glow — layered text-shadow or box-shadow.

LIVE — Glassmorphism

Glass Card

backdrop-filter: blur(16px)
background: rgba(255,255,255,.06)

Neon Glow Text
Neon CSS
Glow Button
glass-grain-glow.css
/* ── GLASSMORPHISM ───────────────────────────── */
.glass-card {
  background: rgba(255, 255, 255, 0.06);
  backdrop-filter: blur(16px) saturate(1.8);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgba(255,255,255,0.15);
  /* MUST have a colorful background BEHIND it */
}

/* ── GRAIN TEXTURE ───────────────────────────── */
.grain::after {
  content: '';
  position: absolute; inset: 0;
  background-image: url("data:image/svg+xml,...");
  /* SVG feTurbulence noise filter */
  background-size: 200px;
  pointer-events: none; opacity: 0.08;
}

/* ── NEON GLOW TEXT ──────────────────────────── */
.neon {
  color: #fff;
  text-shadow:
    0 0 8px  var(--cyan),
    0 0 24px var(--cyan),
    0 0 48px rgba(0,212,200,0.4);
}

/* ── BOX GLOW ────────────────────────────────── */
.glow-btn {
  box-shadow: 0 0 20px rgba(155,89,255,0.5);
}
.glow-btn:hover {
  box-shadow: 0 0 40px rgba(155,89,255,0.8);
}

§ 18 — ::before + ::after + attr()

CSS-Only Tooltips

Tooltips using data-tooltip attributes and CSS attr(). The ::before pseudo-element shows the text; ::after adds the arrow. Zero JavaScript, works on any element.

LIVE — Hover each element for tooltip
Any element →

§ 19 — :has() + grid-template-rows animation

Animated Accordion

The smoothest CSS accordion technique — animate grid-template-rows from 0fr to 1fr. The :has(:checked) selector watches a hidden checkbox inside each item. CSS-only and animation-smooth.

LIVE — CSS-only animated accordion
Start with your industry and emotional goal. Tech companies often use blue (trust) or orange (energy). Pick a hue in HSL, set saturation to 75-85%, lightness to 50-60%. Then derive tints (increase L) and shades (decrease L) from that single H value for a cohesive scale.
Yes — at minimum, respect the OS preference with prefers-color-scheme. Dark mode reduces eye strain in low-light. If adding full dark mode, use semantic tokens so you only change variable values, not every component style. Avoid pure #000 black — dark gray (#111-#1a1a) feels more premium.
The 4px grid is the industry standard. Define a CSS variable scale: --space-1=4px, --space-2=8px, --space-4=16px, --space-8=32px. Use only these values for padding, margin, and gap. This creates automatic visual rhythm — elements will look like they "belong together" without effort.
Four things elevate buttons: (1) translateY(-1px) on hover creates a "lift" with a subtle box-shadow change; (2) transition background-color and box-shadow separately at different speeds for naturalness; (3) add a subtle border even on solid buttons for definition; (4) use letter-spacing: .3px on uppercase labels for a typographic touch.

§ 20 — More CSS-only UI patterns

Ratings & Progress Bars

A CSS-only star rating uses the reverse sibling selector trick — radio inputs laid out in reverse order, so :checked ~ label highlights the right number of stars. Progress bars use CSS animation.

CSS-Only Star Rating

Click any star — no JavaScript

Trick: Radio inputs are in reverse DOM order but displayed flex-direction:row-reverse. So :checked ~ label flows backwards, highlighting all stars before the selected one.
Animated Progress Bars