◆ Beyond the basics
CSS3 IN DEPTH — THE MODERN SPEC
The fundamentals are easy. This is everything past them: native nesting, the :has() parent selector, cascade layers, container queries, subgrid, scroll-snap, color-mix, masking, and the View Transitions API — all rendered live, not simulated.
§ 01
Native Nesting
CSS no longer needs Sass for nesting. Native nesting (Baseline 2023) lets you write child selectors, pseudo-classes, and media queries directly inside a rule — using & to reference the parent.
Card One
Hover to see the nested & selector trigger a border + background change.
nested :hoverCard Two
All styling — including the hover state — lives inside one nested rule.
nested :hoverCard Three
No preprocessor. No build step. Just CSS, shipped natively.
nested :hover/* ── NATIVE NESTING — no Sass needed ── */ .nd-card { background: #1a1025; border: 1px solid #3d2c5a; transition: all 0.3s; /* & = reference to .nd-card itself */ &:hover { border-color: var(--violet); background: #20162f; } /* nested descendant selector */ & h4 { color: var(--violet2); font-family: monospace; } /* nested media query */ @media (max-width: 600px) { padding: 10px; } /* relative nesting — & can appear mid-selector */ .dark & { background: black; } } /* Nesting also works for at-rules, no & required */ .parent { @media (prefers-color-scheme: dark) { color: white; } @supports (gap: 1px) { display: flex; } }
& prefix or :is() wrapping — p { color: red; } alone inside another rule is invalid; write & p instead.§ 02
:has() — The Parent Selector
For 25 years CSS couldn't select a parent based on its children. :has() (Baseline 2023) fixes that — it's a relational pseudo-class that matches an element if the selector inside matches something within it.
/* Style a card when its checkbox is checked — pure CSS! */ .card:has(input:checked) { border-color: lime; box-shadow: 0 0 20px rgba(204,255,51,.2); } /* Form row glows red/green based on input validity */ .form-row:has(input:invalid:not(:placeholder-shown)) { border-color: red; } .form-row:has(input:valid:not(:placeholder-shown)) { border-color: lime; } /* Style a figure differently if it HAS a caption */ figure:has(figcaption) { padding-bottom: 0; } /* Select an article that contains an image */ article:has(img) { grid-column: span 2; } /* :has() as "previous sibling" selector — finally! */ h2:has(+ p) { margin-bottom: 0.5em; } /* h2 immediately followed by p */ /* Combine with :not() for powerful exclusions */ .list-item:not(:has(.badge)) { opacity: 0.6; } /* Quantity queries — style based on sibling count */ .grid:has(> :nth-child(5)) { grid-template-columns: repeat(5, 1fr); }
§ 03
Cascade Layers
@layer gives you explicit control over the cascade — independent of selector specificity or source order. A rule in a later-declared layer always beats an earlier layer, no matter how specific the selector.
/* Declare layer order up front — this order is what matters, */ /* NOT the order the layers are later defined in the file! */ @layer reset, base, components, utilities; @layer base { h1 { font-size: 2rem; } } @layer components { /* Even though .btn-primary is MORE specific than the */ /* utilities rule below, utilities comes LATER in the */ /* @layer declaration, so it wins. */ .btn.btn-primary { background: blue; } } @layer utilities { .bg-red { background: red !important; } } /* Import directly into a layer */ @import url(reset.css) layer(reset); /* Nest layers */ @layer framework { @layer base, components; @layer base { body { margin: 0; } } } /* reference as framework.base */
§ 04
Container Queries
Media queries respond to the viewport. Container queries respond to the element's own container — true component-level responsiveness, independent of where it's placed on the page.
/* 1. Establish a containment context on the PARENT */ .card-wrapper { container-type: inline-size; /* watch width only */ container-name: card-wrap; /* optional name */ } /* 2. Query the CONTAINER, not the viewport */ @container card-wrap (min-width: 380px) { .card { flex-direction: row; } .thumb { width: 100px; } } @container card-wrap (min-width: 560px) { .card { padding: 22px; } } /* Container query LENGTH UNITS */ .title { font-size: clamp(1rem, 5cqw, 2rem); /* cqw = % of container width */ } /* cqw, cqh, cqi, cqb, cqmin, cqmax all available */ /* Container STYLE queries (newer) — query custom properties! */ @container style(--theme: dark) { .card { background: black; } }
§ 05
Logical Properties
Instead of physical directions (left/right/top/bottom), logical properties use inline/block — flow-relative terms that automatically adapt to writing direction and language.
border-inline-start is on the LEFT
SAME CSS — border-inline-start is now on the RIGHT
/* ── PHYSICAL (old way) — breaks in RTL/vertical writing ── */ .card-old { margin-left: 16px; padding-top: 8px; border-right: 2px solid red; text-align: left; } /* ── LOGICAL (modern way) — adapts automatically ── */ .card-new { margin-inline-start: 16px; /* "start" not "left" */ padding-block-start: 8px; /* "block" = vertical flow */ border-inline-end: 2px solid red; text-align: start; } /* ── SHORTHAND VERSIONS ── */ .box { margin-inline: 16px; /* left + right (in LTR) */ margin-block: 8px; /* top + bottom */ inset-inline: 0; /* left:0; right:0; */ inline-size: 300px; /* width (in horizontal writing) */ block-size: 200px; /* height */ } /* Logical border-radius corners */ .box { border-start-start-radius: 8px; } /* top-left in LTR */
dir or writing-mode attribute.§ 06
Color Spaces & color-mix()
Beyond hex and rgb: oklch() is a perceptually uniform color space (equal numeric change = equal visual change), and color-mix() blends any two colors in any color space.
/* ── OKLCH — perceptually uniform, wide gamut ── */ /* oklch(Lightness Chroma Hue) */ .brand { color: oklch(70% 0.2 290); } /* a violet */ /* Lightening/darkening is now PREDICTABLE — just change L */ .brand-light { color: oklch(85% 0.2 290); } .brand-dark { color: oklch(40% 0.2 290); } /* (with hsl(), equal lightness changes LOOK uneven across hues) */ /* ── color-mix() — blend any two colors ── */ .tint { background: color-mix(in srgb, blue 30%, white); } .shade { background: color-mix(in srgb, blue 70%, black); } /* Mixing in oklch gives smoother, more natural gradients */ .smooth-mix { background: color-mix(in oklch, red, blue); } /* Mix with a CSS variable — instant dynamic theming */ .hover-state { background: color-mix(in srgb, var(--brand) 85%, black); } /* relative color syntax (newer) — derive from existing color */ .derived { --base: oklch(60% 0.15 250); background: oklch(from var(--base) calc(l + 0.2) c h); }
§ 07
Aspect Ratio
The aspect-ratio property (Baseline 2021) replaces the old "padding-top percentage hack" for maintaining proportions — works for images, videos, divs, anything.
/* Set width, height is computed automatically */ .video-embed { width: 100%; aspect-ratio: 16 / 9; } /* Square thumbnail regardless of source image shape */ .avatar { aspect-ratio: 1; /* shorthand for 1/1 */ object-fit: cover; /* crop to fill */ border-radius: 50%; } /* OLD HACK (pre-2021) — don't do this anymore */ .old-16-9 { position: relative; padding-top: 56.25%; /* 9/16 = 0.5625 */ } .old-16-9 iframe { position: absolute; inset: 0; } /* aspect-ratio respects min/max constraints */ .flexible { aspect-ratio: 16/9; min-height: 200px; /* overrides ratio if needed */ }
§ 08
Subgrid
Nested grid items normally can't align to their parent grid's tracks. subgrid (Baseline 2023) lets a child inherit its parent's row/column tracks — perfect for card rows that need aligned headers/footers regardless of content length.
/* Parent defines the row tracks */ .card-row { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: auto auto auto; /* img, title, text */ gap: 16px; } /* Each card SPANS the parent's row tracks via subgrid */ .card { display: grid; grid-row: span 3; grid-template-rows: subgrid; /* ← inherits parent's row sizing */ } /* Without subgrid, each card's internal rows size to ITS OWN */ /* content — titles/text wouldn't align across cards. Subgrid */ /* forces every card's row 2 (title) to match the tallest one. */ /* Subgrid also works for columns */ .nested { grid-template-columns: subgrid; grid-column: span 4; }
§ 09
Scroll Snap
Build native carousels, image galleries, and paginated sections with zero JavaScript — the browser handles momentum, snapping, and accessibility.
/* ── PARENT: defines snap axis and strictness ── */ .carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; /* x|y|both, mandatory|proximity */ scroll-behavior: smooth; gap: 14px; } /* ── CHILDREN: define snap alignment ── */ .carousel-item { scroll-snap-align: center; /* start | center | end */ scroll-snap-stop: always; /* prevents skipping items on fast swipe */ flex: 0 0 220px; } /* Full-page vertical snap sections (great for storytelling) */ html { scroll-snap-type: y mandatory; } section { height: 100vh; scroll-snap-align: start; } /* Snap padding — leaves room for a sticky header */ .scroller { scroll-padding-top: 80px; }
§ 10
Shape-Outside
shape-outside lets text wrap around non-rectangular paths — circles, polygons, even the alpha channel of an image — something only print design could do before CSS Shapes.
.float-circle { float: left; width: 130px; height: 130px; shape-outside: circle(50%); clip-path: circle(50%); /* visually clip to match */ shape-margin: 10px; /* gap between shape and text */ } /* Polygon shape — text flows around a custom silhouette */ .float-triangle { shape-outside: polygon(50% 0%, 100% 100%, 0% 100%); } /* Wrap text around the ALPHA CHANNEL of an image */ .float-image { shape-outside: url("portrait.png"); shape-image-threshold: 0.5; /* alpha cutoff */ } /* Inset shape — text wraps around a rounded box */ .float-card { shape-outside: inset(0 round 20px); }
§ 11
@property — Typed Custom Properties
Regular CSS variables are just strings — the browser can't animate between two gradient angles or interpolate a color smoothly. @property gives a custom property a real type, making it animatable.
/* Register the custom property with a TYPE */ @property --angle { syntax: '<angle>'; /* the browser now understands this as an angle */ initial-value: 0deg; inherits: false; } .spinner { background: conic-gradient(from var(--angle), violet, magenta, lime, violet); animation: spin 4s linear infinite; } @keyframes spin { to { --angle: 360deg; } /* THIS is what's impossible with a plain --angle */ } /* Common syntax types you can register: */ /* '<number>' '<percentage>' '<length>' '<color>' '<angle>' */ /* '<integer>' '<time>' or '*' for untyped (old behavior) */ @property --progress-color { syntax: '<color>'; initial-value: red; inherits: true; } .progress-bar { background: var(--progress-color); transition: --progress-color 0.5s; /* smooth color transition now works! */ } .progress-bar.complete { --progress-color: lime; }
§ 12
Masking
CSS masks use the luminance or alpha of an image/gradient to determine visibility — black hides, white shows, gray is partial. Far more flexible than clip-path for fades and complex cutouts.
/* ── FADE OUT — linear gradient mask ── */ .fade-right { mask-image: linear-gradient(to right, black, transparent); /* black = fully visible, transparent = fully hidden */ } /* ── VIGNETTE — radial gradient mask ── */ .vignette { mask-image: radial-gradient(circle, black 40%, transparent 70%); } /* ── SHAPE-FROM-IMAGE — mask with any PNG/SVG ── */ .masked-photo { mask-image: url("star-shape.svg"); mask-size: contain; mask-repeat: no-repeat; } /* ── COMBINE MULTIPLE MASKS ── */ .complex-mask { mask-image: linear-gradient(to bottom, black 80%, transparent), radial-gradient(circle at top right, transparent 10%, black 11%); mask-composite: intersect; } /* mask vs clip-path: mask supports gradients/partial opacity, */ /* clip-path is purely binary (a point is in or out of the path) */ /* Safari still needs the prefix for full support */ .cross-browser { -webkit-mask-image: linear-gradient(black, transparent); mask-image: linear-gradient(black, transparent); }
§ 13
View Transitions API
Native, app-like animated transitions between DOM states — the browser automatically crossfades and morphs elements that share a view-transition-name, no animation library required.
// ── BASIC USAGE (works for SPA-style state swaps) ── function updateView(newState) { if (!document.startViewTransition) { applyState(newState); // fallback for unsupported browsers return; } document.startViewTransition(() => { applyState(newState); // DOM mutation happens here }); // browser automatically captures before/after screenshots // and cross-fades/morphs between them }
/* Name an element so the browser tracks it across the transition */ .hero-image { view-transition-name: hero-img; } /* Customize the default crossfade animation */ ::view-transition-old(hero-img) { animation: fade-out 0.3s; } ::view-transition-new(hero-img) { animation: fade-in 0.3s; } /* Multi-page navigation transitions (Chrome 126+) */ @view-transition { navigation: auto; } /* enables it for normal page navigations */
§ 14
Performance CSS
Modern CSS properties that directly impact rendering performance — content-visibility can skip rendering work for off-screen content entirely, and contain limits the scope of layout recalculation.
/* ── content-visibility — skip rendering off-screen content ── */ .long-article section { content-visibility: auto; contain-intrinsic-size: 0 500px; /* placeholder size to avoid layout shift */ } /* Browser skips layout/paint for sections not in/near viewport */ /* — can cut initial render time by 50%+ on long pages */ /* ── contain — isolate an element's rendering scope ── */ .widget { contain: layout; /* changes inside don't affect outside layout */ contain: paint; /* clips overflow, isolates paint */ contain: size; /* size doesn't depend on children */ contain: strict; /* shorthand: size + layout + paint */ contain: content; /* shorthand: layout + paint */ } /* ── will-change — hint the browser to optimize ahead of time ── */ .about-to-animate { will-change: transform, opacity; } /* ⚠ use sparingly — remove after animating, overuse hurts memory */ /* ── GPU-accelerated properties (animate these, not top/left) ── */ .smooth-move { transform: translateX(100px); /* compositor-only, no layout/paint */ opacity: 0.5; /* also compositor-only */ } /* AVOID animating: width, height, top, left, margin — these */ /* trigger layout recalculation on every frame */
§ 15
User Preference Media Features
CSS can read the visitor's OS-level accessibility and display preferences — respecting them is both good UX and, increasingly, a legal accessibility requirement.
/* ── Color scheme preference ── */ @media (prefers-color-scheme: dark) { :root { --bg: #111; --text: #eee; } } /* ── Reduced motion — respect vestibular disorders ── */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } /* ── Reduced transparency ── */ @media (prefers-reduced-transparency: reduce) { .glass-panel { backdrop-filter: none; background: solid; } } /* ── Contrast preference ── */ @media (prefers-contrast: more) { body { --border-color: black; } } /* ── Pointer type — adapt for touch vs mouse ── */ @media (pointer: coarse) { /* touch */ .btn { min-height: 44px; } /* bigger tap targets */ } @media (hover: hover) { /* device CAN hover */ .card:hover { transform: scale(1.02); } } /* ── Forced colors (Windows High Contrast Mode) ── */ @media (forced-colors: active) { .custom-checkbox { forced-color-adjust: none; } }