◆ 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.

CSS Nesting :has() / :is() Container Queries ● Live in your browser

§ 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.

LIVE — Hover the cards

Card One

Hover to see the nested & selector trigger a border + background change.

nested :hover

Card Two

All styling — including the hover state — lives inside one nested rule.

nested :hover

Card Three

No preprocessor. No build step. Just CSS, shipped natively.

nested :hover
nesting.css
/* ── 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;
  }
}
Gotcha: a nested rule starting with a type selector needs the & 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.

LIVE — Check a box, watch its card glow
LIVE — Form validation via :has()
has-selector.css
/* 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.

VISUAL — Layer Priority (bottom = lowest, top = wins)
overrideshighest priority — always wins
componentsyour design system
baseresets, typography
(unlayered styles)paradoxically: HIGHEST priority of all!
Counter-intuitive rule: any CSS not in a layer beats all layered CSS, regardless of specificity. This makes layers perfect for organizing third-party/reset/component CSS while keeping your page-specific overrides simple and unlayered.
layers.css
/* 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.

LIVE — Drag the bottom-right corner to resize ↘
Responsive Card
This card's own LAYOUT changes based on its container's width — not the viewport. Resize the dashed box to see it switch from stacked → row → spacious.
container-queries.css
/* 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.

LIVE — Same CSS, different direction attribute
direction: ltr (default)
border-inline-start is on the LEFT
direction: rtl
SAME CSS — border-inline-start is now on the RIGHT
logical-properties.css
/* ── 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 */
Why it matters: if your site ever supports Arabic, Hebrew, or vertical Japanese text, logical properties mean zero CSS rewriting — the layout flips automatically with the 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.

LIVE — color-mix() in different spaces
color-spaces.css
/* ── 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.

LIVE — Common Ratios
1 / 1
16 / 9
4 / 3
21 / 9
aspect-ratio.css
/* 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.

LIVE — Card titles & text align across cards despite different content length
Short Title
Brief description text.
A Much Longer Card Title Here
A longer description that wraps onto multiple lines to demonstrate that rows still align.
Medium Title
Medium length description text here.
subgrid.css
/* 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.

LIVE — Scroll horizontally, watch it snap into place
01
02
03
04
05
scroll-snap.css
/* ── 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.

LIVE — Text flows around the circle, not its bounding box
Notice how this paragraph text doesn't wrap around a rectangular box — it follows the actual curve of the circle. This is shape-outside in action: the float still creates a rectangular bounding area, but shape-outside tells inline content to flow around the circular shape inside it instead. You can use circle(), ellipse(), polygon(), inset(), or even reference the alpha channel of a transparent PNG to wrap text around a complex illustration's silhouette. This technique was historically exclusive to desktop publishing tools and is now native to the web platform with zero JavaScript required.
shape-outside.css
.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.

LIVE — Animating a conic-gradient angle (impossible without @property)
at-property.css
/* 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.

LIVE — Mask Examples
linear fade mask radial vignette mask text-shaped mask
masking.css
/* ── 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.

LIVE — Click a thumbnail (uses real View Transition API if supported)
view-transitions.js
// ── 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
}
view-transitions.css
/* 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.

performance.css
/* ── 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 */
CONCEPTUAL — Relative render cost by technique
animating top/left
animating width/height
box-shadow change
transform / opacity

§ 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.

YOUR ACTUAL SYSTEM SETTINGS — detected live
media-features.css
/* ── 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; }
}