../

CSS

Modern CSS as of September 2026: the cascade, layers, selectors, layout, color, queries and the newer platform features, with their Baseline status taken from MDN. Utility-first styling is in Tailwind CSS, motion in Animation and palettes in Color theory.

Cascade & specificity

When two declarations set the same property on an element, the cascade checks these steps in order and stops at the first one that tells them apart.

checked first tie? go down one step; the first step with a difference decides 1 origin & importance 2 context 3 element-attached 4 cascade layers 5 specificity 6 scope proximity 7 order of appearance !important beats normal light DOM beats shadow DOM style="" beats any selector unlayered > last layer > first (id, class, type), left first nearer @scope root wins the later rule wins
The cascade: the first step with a difference decides
StepWinner
1. Origin & importancetransitions, then !important (UA, then user, then author), then animations, then normal author, user and UA styles
2. Contextfor normal rules the outer (light DOM) wins over shadow DOM; for !important the shadow DOM wins
3. Element-attacheda style="" attribute beats any selector
4. Cascade layersunlayered beats the last layer, which beats earlier layers (this flips for !important)
5. Specificitythe higher (id, class, type) tuple
6. Scope proximitythe @scope root closer to the element
7. Order of appearancethe rule that comes later
#nav .item a:hover A · IDs B · classes C · types #id .class [attr] :hover div ::before 1 2 1 #nav .item :hover a specificity 1-2-1 *, combinators and :where() add 0; :is() and :not() add their argument Compare column by column, from the left A B C #main .a.b.c.d.e.f.g.h.i.j.k 1 0 0 0 11 0 wins A differs (1 vs 0), so B and C are never compared: 11 classes don't carry over
Specificity: three counters compared from the left
SelectorSpecificity
*, :where(...), combinators0-0-0
p, ::before0-0-1
.btn, [type="text"], :hover0-1-0
#nav1-0-0
:is(#a, .b), :not(...), :has(...)the most specific argument (1-0-0 here)
:nth-child(2n of .x)0-1-0 plus the most specific argument
nav a.active:hover0-2-2
nested &behaves like :is(parent), so #a, p { & span {} } scores 1-0-1
/* keep defaults easy to override: 0-0-0 */
:where(ul[role="list"]) { list-style: none; }
 
/* bump without adding an id: 0-2-0 */
.btn.btn { color: red; }
 
/* reset a property to the previous layer's value */
.card { color: revert-layer; }
KeywordResets to
inheritthe parent's computed value
initialthe spec's initial value (for display that is inline)
unsetinherit for inherited properties, otherwise initial
revertthe user-agent or user stylesheet value
revert-layerthe value from the previous cascade layer

Cascade layers

@layer fixes the precedence between groups of rules, so specificity only competes inside a layer. Declare the order once, up front.

styles/main.css
@layer reset, tokens, base, components, utilities;
 
@import url("reset.css") layer(reset);
@import url("tokens.css") layer(tokens);
@import url("base.css") layer(base);
 
@layer components {
  .btn { padding: 0.5rem 1rem; }
  @layer forms {            /* components.forms */
    input { font: inherit; }
  }
}
 
@layer utilities {
  /* 0-1-0, yet beats any components rule */
  .hidden { display: none; }
}
 
/* unlayered: beats every layer (normal declarations) */
.legacy-widget #thing { color: red; }
layered styles folder
styles/main.css            # only linked file: @layer, @importsreset.css           # layer(reset)tokens.css          # custom properties, @propertybase.css            # element defaults, typographycomponents/button.css      # @layer components { ... }card.cssutilities.css       # @layer utilitiesvendor/datepicker.css  # @import ... layer(vendor)
RuleDetail
First mention sets order@layer a, b; must come before any rule that creates a or b
Later layer winsfor normal declarations, whatever the specificity
!important flips itan important declaration in an earlier layer beats one in a later layer, and both beat unlayered important
Unlayered stylesact as an implicit last layer: third-party CSS can override yours, so wrap vendors in a layer
Nested@layer a.b or @layer a { @layer b {} }
Anonymous@layer { ... } can't be reopened or reordered later

A 1-2-0 selector in an earlier layer loses to a 0-1-0 one in a later layer:

layers.html
<style>
  @layer base, components;
 
  @layer components {
    .btn { background: var(--graph-2); }       /* 0-1-0 */
  }
  @layer base {
    #save.btn.primary { background: var(--graph-1); }
  }
 
  .btn { padding: 8px 14px; border: 0; border-radius: 4px;
    font: inherit; color: var(--bg); }
</style>
<button id="save" class="btn primary">
  Green: the later layer wins
</button>
Result

Selectors, nesting & @scope

SelectorMatches
a b / a > bdescendant / direct child
a + b / a ~ bnext sibling / any later sibling
[attr], [attr="v"]attribute present / equals
[attr^="v"], [attr$="v"], [attr*="v"]starts with / ends with / contains
[attr="v" i]case-insensitive match
:is(h1, h2) aany argument matches; takes the highest argument's specificity; ignores invalid arguments
:where(h1, h2) alike :is with zero specificity, good for resets and libraries
:not(.a, .b)matches neither argument
:has(> img)the parent that has a direct img child
label:has(+ input:invalid)the previous sibling (a label before an invalid input)
:has(:focus-visible)any ancestor of the focused element
:nth-child(2n+1 of .item)odd items among .item only
:first-child, :last-child, :only-childposition among siblings
:nth-of-type(3), :last-of-typeposition among same-tag siblings
:emptyno children and no text (whitespace counts as text)
:focus-visiblekeyboard-style focus; use it for focus rings
:focus-withinthe element or a descendant has focus
:targetthe element whose id matches the URL fragment
:roothtml; where global tokens go
::before, ::aftergenerated content; needs content
::marker, ::placeholder, ::selection, ::backdroplist bullet, input hint, selected text, top-layer backdrop
/* card grid: style the card when its checkbox is ticked */
.card:has(input[type="checkbox"]:checked) {
  outline: 2px solid var(--accent);
}
 
/* no scroll behind an open modal */
html:has(dialog[open]:modal) { overflow: hidden; }
 
/* form rows with an error */
.field:has(:user-invalid) .hint { color: var(--danger); }

Nesting (Baseline 2023)

.card {
  padding: 1rem;
 
  & h2 { margin-block: 0; }    /* .card h2 */
  h2 { margin-block: 0; }      /* same thing: & optional */
  &:hover { background: var(--hover); }
  &.is-active { border-color: var(--accent); }
  .theme-dark & { color: white; }   /* .theme-dark .card */
 
  @media (width >= 40rem) { padding: 2rem; }
  @container (width >= 30rem) { display: grid; }
}

Unlike Sass, & can't build class names by concatenation: &__title doesn't mean .card__title. Use a real class.

@scope (Baseline 2026)

Styles that apply between a scope root and an optional lower limit. When two scopes match, the closer root wins (scope proximity) before source order is considered.

@scope (.card) to (.card__slot) {
  :scope { border: 1px solid var(--line); }
  img { border-radius: 0.5rem; } /* not in .card__slot */
}
 
/* inline: scoped to the parent of the style element */
/* <div><style>@scope { p { color: red } }</style></div> */

Custom properties

SyntaxUse
--gap: 1rem;declare; it inherits by default and cascades like any property
var(--gap)read it
var(--gap, 1rem)fallback when it isn't set
var(--a, var(--b, 0))fallback chain
--x: ;a valid empty value (the "space toggle" trick)
el.style.setProperty("--x", "4px")set it from JS
getComputedStyle(el).getPropertyValue("--x")read it from JS

An unregistered property is just a token stream: it can't be animated and it isn't type-checked. @property (Baseline 2024) registers a type, which makes it animatable and lets you opt out of inheritance.

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
 
.spinner {
  background: conic-gradient(
    from var(--angle), var(--accent), transparent
  );
  animation: spin 2s linear infinite;
}
@keyframes spin { to { --angle: 360deg; } }
syntaxAccepts
"<length>", "<percentage>", "<length-percentage>"10px, 50%
"<number>", "<integer>"1.5, 3
"<color>"any color; animates smoothly
"<angle>", "<time>"45deg, 200ms
"<length>+" / "<length>#"space- / comma-separated list
"auto | <length>"either one
"*"anything (the same as unregistered)

Box model & units

margin padding content width × height border
The box model

Both boxes below declare width: 200px; the red bar is 200px wide.

box-sizing.html
<style>
  .ruler { width: 200px; height: 4px; margin-bottom: 6px;
    background: var(--graph-1); }
  .box { width: 200px; padding: 12px; margin-bottom: 8px;
    border: 6px solid var(--graph-0);
    background: var(--chip); font-size: 13px; }
  .content { box-sizing: content-box; }  /* 236px */
  .border { box-sizing: border-box; }    /* 200px */
</style>
<div class="ruler"></div>
<div class="box content">content-box: 236px</div>
<div class="box border">border-box: 200px</div>
Result
PropertyNote
box-sizing: border-boxwidth includes padding and border; set it globally
margincollapses vertically between blocks (never in flex or grid)
outlinetakes no layout space; use outline-offset for focus rings
aspect-ratio: 16 / 9height follows width
min-width: 0lets flex and grid children shrink below their content size
overflow: cliplike hidden but creates no scroll container, so sticky still works
display: flow-rootnew block formatting context: contains floats and stops margin collapse
display: contentsthe box disappears and its children join the parent's layout
*, *::before, *::after { box-sizing: border-box; }
img, svg, video { display: block; max-width: 100%; }
UnitRelative toUse for
pxCSS pixelhairlines, shadows
remroot font sizetype, spacing (follows the user's zoom and font settings)
emthe element's font sizepadding that scales with text, icons
chwidth of 0line length: max-inline-size: 65ch
ex, capx-height, cap heighticon alignment
lh, rlhthe element's / root line heightvertical rhythm: margin-block: 1lh
%the parent (the property decides which size)fluid widths
vw, vh1% of the viewportfull-bleed; vw ignores the scrollbar
svh, lvh, dvhsmallest / largest / current viewport (mobile toolbars)min-block-size: 100dvh
vi, vbviewport inline / block axiswriting-mode aware
cqi, cqb1% of the container's inline / block sizetype inside components
cqw, cqh, cqmin, cqmaxcontainer width / height / smaller / larger
fra share of free grid spacegrid tracks
FunctionExample
calc()calc(100% - 2rem)
min() / max()width: min(100%, 60rem)
clamp(min, ideal, max)clamp(1rem, 2.5vw, 2rem)
round(), mod(), rem()round(up, 13px, 4px) gives 16px
sin(), cos(), atan2()circular layouts

Color

SyntaxNote
oklch(70% 0.15 250)lightness, chroma, hue: perceptually even; the best choice for palettes
oklch(70% 0.15 250 / 50%)with alpha
color(display-p3 1 0 0)wide-gamut color
color-mix(in oklch, var(--c) 70%, white)tints, shades and hover states
color-mix(in srgb, var(--c), transparent 50%)alpha from a token
oklch(from var(--c) calc(l + 0.1) c h)relative color: lighten
oklch(from var(--c) l c calc(h + 180))relative color: complement
rgb(from var(--c) r g b / 40%)relative color: swap the alpha
light-dark(#fff, #111)picks by color-scheme; colors only
contrast-color(var(--bg))black or white, whichever contrasts more (Baseline 2026)
currentColorthe element's color
accent-colortints checkboxes, radios, range and progress
:root {
  color-scheme: light dark;   /* needed by light-dark() */
  --brand: oklch(62% 0.19 255);
  --brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);
  --brand-soft: color-mix(in oklch, var(--brand) 15%, white);
  --bg: light-dark(oklch(99% 0 0), oklch(18% 0.01 255));
  --fg: light-dark(oklch(20% 0.02 255), oklch(94% 0 0));
}

The same derivations, computed live from one token:

relative-color.html
<style>
  :root { --brand: oklch(62% 0.19 255); }
  .row { display: flex; gap: 8px; font: 11px monospace; }
  .row div { flex: 1; }
  .row div::before { content: ""; display: block;
    height: 44px; margin-bottom: 4px; border-radius: 4px;
    background: var(--c); }
</style>
<div class="row">
  <div style="--c: var(--brand)">brand</div>
  <div style="--c: oklch(from var(--brand)
    calc(l - 0.08) c h)">l - 0.08</div>
  <div style="--c: color-mix(in oklch,
    var(--brand) 15%, white)">15% into white</div>
  <div style="--c: oklch(from var(--brand)
    l c calc(h + 180))">h + 180</div>
</div>
Result

oklch chroma above about 0.37 falls outside sRGB and gets gamut-mapped; keep chroma around 0.1–0.2 for UI colors. Contrast rules are covered in Color theory.

Flexbox

A one-dimensional layout: items flow along the main axis and align on the cross axis.

display: flex; flex-direction: row 1 2 3 free space main-start main-end main axis: justify-content cross-start cross-end cross axis: align-items flex-direction: column swaps the two axes
Flex axes: justify-content on the main axis, align-items on the cross axis
justify-content.html
<style>
  .g { display: grid; grid-template-columns: auto 1fr;
    gap: 6px 10px; align-items: center;
    font: 12px monospace; }
  .row { display: flex; gap: 4px; padding: 4px;
    background: var(--chip); border-radius: 4px; }
  .row i { width: 36px; height: 16px; border-radius: 3px;
    background: var(--graph-0); }
</style>
<div class="g">
  <span>flex-start</span>
  <div class="row"><i></i><i></i><i></i></div>
  <span>center</span>
  <div class="row" style="justify-content: center">
    <i></i><i></i><i></i></div>
  <span>space-between</span>
  <div class="row" style="justify-content: space-between">
    <i></i><i></i><i></i></div>
  <span>space-around</span>
  <div class="row" style="justify-content: space-around">
    <i></i><i></i><i></i></div>
  <span>space-evenly</span>
  <div class="row" style="justify-content: space-evenly">
    <i></i><i></i><i></i></div>
</div>
Result
PropertyValues / effect
display: flex / inline-flexmakes a flex container
flex-directionrow (default), column, row-reverse, column-reverse
flex-wrapnowrap (default), wrap
flex-flowshorthand: row wrap
gapspace between items: gap: 1rem 2rem (row, column)
justify-contentmain axis: flex-start, center, space-between, space-around, space-evenly
align-itemscross axis: stretch (default), center, flex-start, baseline, last baseline
align-contentspacing between wrapped lines
align-selfone item's cross-axis alignment
flex: 11 1 0%: share space equally, ignoring content size
flex: auto1 1 auto: grow from content size
flex: none0 0 auto: rigid
flex: 0 0 16remfixed basis, never grows or shrinks
ordervisual order only; screen readers and tabbing keep DOM order
margin-inline-start: autopushes the item and everything after it to the end
min-width: 0 on an itemlets text truncate instead of overflowing
.toolbar { display: flex; align-items: center; gap: 0.5rem; }
.toolbar .spacer { margin-inline-start: auto; }
 
.media { display: flex; gap: 1rem; }
.media > img { flex: none; inline-size: 4rem; }
.media > .body { flex: 1; min-width: 0; }

Grid

A two-dimensional layout: define tracks on the container and place items into them.

1 2 3 4 -4 -3 -2 -1 1 2 3 track column 1 area grid-column: 2 / 4 cell cell gap lines are numbered from 1; negative numbers count from the end
Grid lines, tracks, cells, areas and gaps
PropertyExample / effect
grid-template-columnsrepeat(3, 1fr), 16rem 1fr, [full-start] 1fr [full-end]
repeat(auto-fit, minmax(16rem, 1fr))as many columns as fit; empty tracks collapse so items stretch
repeat(auto-fill, ...)the same, but keeps empty tracks
minmax(0, 1fr)a 1fr that can shrink below its content's min size
grid-template-rowsauto 1fr auto for header, body, footer
grid-template-areasASCII map of named areas; . is an empty cell
grid-area: mainplace an item into a named area
grid-column: 1 / -1span the full explicit grid
grid-column: span 2span two tracks from the auto-placed start
grid-auto-rows: minmax(8rem, auto)size of implicit rows
grid-auto-flow: densebackfill holes (visual order no longer matches the DOM)
grid-template-columns: subgrida child reuses the parent's tracks (Baseline 2023)
place-items: centeralign-items plus justify-items
place-content: centeraligns the whole grid inside the container
justify-self, align-selfone item's alignment
.layout {
  display: grid;
  grid-template:
    "header header" auto
    "nav    main"   1fr
    "footer footer" auto
    / 14rem 1fr;
  min-block-size: 100dvh;
}
.layout > header { grid-area: header; }
.layout > nav    { grid-area: nav; }
.layout > main   { grid-area: main; }
.layout > footer { grid-area: footer; }
grid-areas.html
<style>
  .layout { display: grid; gap: 6px; height: 150px;
    grid-template:
      "head head" auto
      "nav  main" 1fr
      "foot foot" auto
      / 90px 1fr; }
  .layout > * { padding: 6px 10px; border-radius: 4px;
    background: color-mix(in srgb, var(--c) 25%,
      transparent); }
  header { grid-area: head; --c: var(--graph-0); }
  nav    { grid-area: nav;  --c: var(--graph-2); }
  main   { grid-area: main; --c: var(--graph-3); }
  footer { grid-area: foot; --c: var(--graph-1); }
</style>
<div class="layout">
  <header>header</header><nav>nav</nav>
  <main>main</main><footer>footer</footer>
</div>
Result
subgrid: card parts line up across a row
.cards {
  display: grid;
  grid-template-columns:
    repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1rem;
}
.card {
  display: grid;
  grid-row: span 3;               /* title, body, footer */
  grid-template-rows: subgrid;
}

Positioning, stacking & anchors

positionOffsets fromIn flow?
static(ignores inset)yes
relativeits own normal positionyes (its space is kept)
absolutethe nearest positioned ancestorno
fixedthe viewport, or an ancestor with transform, filter or containno
stickythe nearest scrolling ancestor, once past its thresholdyes

inset: 0 is short for top/right/bottom/left: 0. Sticky fails if an ancestor between the element and the scroller has overflow: hidden (use clip) or if the element has no room to move inside its parent.

position.html
<style>
  .scroller { height: 146px; overflow: auto;
    border: 1px solid var(--muted); border-radius: 4px; }
  .sticky { position: sticky; top: 0; z-index: 1;
    padding: 6px 10px; background: var(--graph-0);
    color: var(--bg); }
  .card { position: relative; margin: 14px 10px;
    padding: 12px 10px; border-radius: 4px;
    background: var(--chip); }
  .badge { position: absolute; top: -8px; right: -4px;
    padding: 0 6px; border-radius: 9px; font-size: 12px;
    background: var(--graph-1); color: var(--bg); }
</style>
<div class="scroller">
  <div class="sticky">sticky: scroll this box</div>
  <div class="card">relative card
    <span class="badge">absolute</span></div>
  <div class="card">relative card
    <span class="badge">absolute</span></div>
  <div class="card">relative card
    <span class="badge">absolute</span></div>
</div>
Result

Stacking contexts

z-index only competes among siblings in the same stacking context.

root stacking context ├ A z-index: 1 │ └ badge z-index: 999 └ B z-index: 2 A z-index: 1 badge 999 B z-index: 2 A creates a context, so 999 only ranks the badge among A's own children siblings A (1) and B (2) decide the order, so B covers the badge
z-index only compares within one stacking context

These create a new stacking context:

TriggerExample
positioned with a z-index other than autoposition: relative; z-index: 1
flex or grid child with a z-indexz-index: 0
position: fixed or stickyalways
opacity less than 1opacity: 0.99
transform, scale, rotate, translate, filter, backdrop-filter, perspective, clip-path, maskany value other than none
mix-blend-mode other than normal
isolation: isolatethe explicit, side-effect-free way
contain: layout or paint; container-type: size or inline-size
will-change naming any of the above
view-transition-name
the top layer (dialog.showModal(), popovers, fullscreen)above everything, no z-index needed
/* keep a component's z-indexes local */
.card { isolation: isolate; }
.card .badge { position: absolute; z-index: 1; }

Anchor positioning

Tether an absolutely positioned element to another element without JS. The core properties work in Chrome 125+, Firefox 147+ and Safari 26+. MDN still labels the whole feature "limited" because some position-visibility values are Safari 27 only.

Property / functionDoes
anchor-name: --menunames the anchor
position-anchor: --menuwhich anchor to use (a popover opened by popovertarget gets its invoker implicitly)
position-area: block-end span-inline-endplace it in a 3×3 grid around the anchor
top: anchor(bottom)position against one of the anchor's edges
justify-self: anchor-centercenter on the anchor
width: anchor-size(width)size from the anchor
position-try-fallbacks: flip-block, flip-inlinealternatives when it would overflow
@position-try --name { ... }a named custom fallback
position-visibility: anchors-visiblehide it when the anchor scrolls out of view
.trigger { anchor-name: --user-menu; }
 
.menu {
  position: absolute;
  position-anchor: --user-menu;
  position-area: block-end span-inline-end;
  position-try-fallbacks: flip-block, flip-inline;
  margin-block-start: 0.25rem;
  min-inline-size: anchor-size(inline);
}

Container & media queries

Container queries

Components respond to their container's size, not the viewport's.

.card-wrap { container: card / inline-size; }
 
@container card (width >= 30rem) {
  .card { grid-template-columns: 10rem 1fr; }
}
 
.card h2 { font-size: clamp(1rem, 4cqi, 1.5rem); }

The same card markup in a 170px slot and in a full-width slot:

container-query.html
<style>
  .slot { container-type: inline-size;
    margin-bottom: 8px; }
  .narrow { width: 170px; }
  .card { display: grid; gap: 8px; padding: 8px;
    border-radius: 6px; background: var(--chip); }
  .img { aspect-ratio: 16 / 9; border-radius: 4px;
    background: var(--graph-2); }
  @container (width >= 300px) {
    .card { grid-template-columns: 120px 1fr;
      align-items: center; }
  }
</style>
<div class="slot narrow"><div class="card">
  <div class="img"></div><b>Stacked</b>
</div></div>
<div class="slot"><div class="card">
  <div class="img"></div><b>Side by side</b>
</div></div>
Result
SyntaxNote
container-type: inline-sizequery width; the common case
container-type: sizequery both axes; the element needs an explicit height
container: name / typeshorthand
@container (width > 40rem)nearest ancestor container of any name
@container sidebar (orientation: portrait)a named container
@container style(--tone: inverted)style query on a custom property (Baseline 2026)
@container scroll-state(stuck: top)sticky or snapped state (Chromium only)
cqi, cqb units1% of the container, without a query

A container can't query itself; style its children. Size containment means the container no longer takes its size from its children on the queried axis.

Media queries

QueryUse
(width >= 48rem)range syntax (Baseline 2023); rather than min-width
(40rem <= width < 64rem)a band
(prefers-color-scheme: dark)OS theme
(prefers-reduced-motion: reduce)tone down motion
(prefers-contrast: more)stronger borders and text
(forced-colors: active)Windows high contrast; use system colors
(prefers-reduced-transparency: reduce)Chromium only
(hover: hover) and (pointer: fine)mouse-like input
(pointer: coarse)touch: bigger hit targets
(orientation: landscape)
(resolution >= 2x)high-DPI assets
(display-mode: standalone)installed PWA
printprint styles

Media queries can't use custom properties: (width >= var(--md)) is invalid. Breakpoints in rem follow the user's font size.

Typography

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-weight: 100 900;       /* variable font range */
  font-style: normal;
  font-display: swap;
}
 
/* metric-matched fallback cuts layout shift */
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
}
 
body { font-family: "Inter", "Inter Fallback", sans-serif; }
<link rel="preload" href="/fonts/inter.woff2"
  as="font" type="font/woff2" crossorigin>
font-displayBehavior
swapfallback shows at once and swaps in when the font loads (body text)
optionaluses the font only if it's already fast or cached; no layout shift
fallbackshort invisible period, short swap window
blockinvisible text for up to about 3s (icon fonts)
PropertyUse
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem)fluid type; keep a rem term so zoom still works
line-height: 1.5unitless, so it scales with the font size
max-inline-size: 65chreadable line length
text-wrap: balanceeven line lengths for short blocks such as headings (Baseline 2024)
text-wrap: prettyavoids orphans in paragraphs (Chromium, Safari 26; not Firefox)
hyphens: autoneeds a lang attribute
font-variant-numeric: tabular-numsaligned digits in tables
font-size-adjust: ex-height 0.53keeps the x-height steady across fallbacks (Baseline 2024)
text-box: trim-both cap alphabetictrim leading above caps and below the baseline (Chromium, Safari)
-webkit-line-clamp: 3multi-line ellipsis (the prefixed form works everywhere)
text-underline-offset, text-decoration-thicknessnicer links
font-optical-sizing: autovariable fonts with an opsz axis

With Next.js, next/font self-hosts fonts and generates the fallback metrics for you; see Next.js.

Logical properties & scrolling

Logical properties follow the writing mode, so RTL and vertical text work without overrides (Baseline 2021).

PhysicalLogical
width / heightinline-size / block-size
margin-left + margin-rightmargin-inline
padding-top + padding-bottompadding-block
margin-left (in LTR)margin-inline-start
top / bottominset-block-start / inset-block-end
left + rightinset-inline
border-top-left-radiusborder-start-start-radius
text-align: lefttext-align: start
min-height: 100vhmin-block-size: 100dvh
PropertyUse
scroll-snap-type: x mandatoryon the scroller: snap along x, always
scroll-snap-align: starton the children: the snap point
scroll-snap-stop: alwaysdon't skip items on a fast swipe
scroll-padding-block-start: 4remon the scroller: offset for a sticky header
scroll-margin-block-start: 4remon targets: the same offset, per element
scroll-behavior: smoothsmooth anchor jumps; wrap it in prefers-reduced-motion: no-preference
overscroll-behavior: containstops scroll chaining and pull-to-refresh (MDN: limited availability)
scrollbar-gutter: stablereserves the scrollbar's space so content doesn't shift (Baseline 2024)
scrollbar-width: thinauto, thin, none (Baseline 2024)
scrollbar-color: thumb trackscrollbar colors (Baseline 2025)
.carousel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 80%;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
}
.carousel > * { scroll-snap-align: center; }

Forms, popover & dialog

Selector / propertyUse
:user-invalid, :user-validonly after the user has interacted or submitted (Baseline 2023); use instead of :invalid
:invalid, :validmatches from page load, so empty required fields show red at once
:required, :optional
:placeholder-shownthe field is empty; floating labels
:checked, :indeterminatecheckboxes, radios and options
:disabled, :read-only
:autofillbrowser-filled fields (not Baseline)
:focus-visiblethe focus ring
accent-color: var(--brand)recolours native controls (all desktop engines; MDN flags it limited)
field-sizing: contentinputs and textareas grow with their content (Baseline 2026)
appearance: nonestrip native styling before restyling
appearance: base-selectfully stylable select (Chromium, Safari 27)
color-scheme: light darknative controls and scrollbars follow the theme
input, textarea, select { font: inherit; }
textarea { field-sizing: content; min-block-size: 3lh; }
 
.field input:user-invalid {
  border-color: var(--danger);
  outline-color: var(--danger);
}

Popover & dialog

SelectorMatches
[popover]:popover-openan open popover (popover API is Baseline 2025)
dialog[open], dialog:modalan open dialog / one opened with showModal()
::backdropthe layer behind a modal or popover
@starting-style { ... }styles for the first frame, so the entry can transition
transition-behavior: allow-discretelets display (Chromium, Safari) and overlay (Chromium) transition; Firefox snaps
<button popovertarget="tip">Info</button>
<div id="tip" popover>Anchored tip</div>
 
<button commandfor="dlg" command="show-modal">Open</button>
<dialog id="dlg" closedby="any">...</dialog>
dialog {
  border: none;
  border-radius: 1rem;
  padding: 1.5rem;
}
dialog::backdrop { background: oklch(0% 0 0 / 40%); }
[popover] { margin: 0; inset: auto; }  /* then anchor it */

commandfor and command are Baseline 2025. closedby (light dismiss for dialogs) is Chromium and Firefox only. Open and close animations are in Animation.

View transitions

view-transition-name: hero gives an element its own snapshot, so it morphs between states or pages. @view-transition { navigation: auto; } opts into cross-document transitions. Details are in Animation.

Performance & browser support

ToolEffect
content-visibility: autoskips rendering off-screen sections (Baseline 2025)
contain-intrinsic-size: auto 40remplaceholder size for skipped content; auto remembers the real size
contain: layout paintisolates a widget's layout and paint work
contain: strictsize + layout + paint + style; needs an explicit size
will-change: transformpromotes to a layer; add it just before animating, not everywhere
transform / opacitythe only properties the compositor can animate cheaply
Tight :has() anchors.card:has(img) is cheap; body:has(...) re-checks the whole tree
@import in CSSblocks: it fetches serially; bundle or link instead
media on linkmedia="print" stylesheets don't block render
.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 24rem;
}
FeatureStatus (MDN, Sept 2026)
Layers, nesting, :has(), container size queries, subgrid, color-mix(), oklch(), range media queries, dvh, lhBaseline widely available
@property, light-dark(), relative color, @starting-style, transition-behavior, text-wrap: balanceBaseline 2024
Popover, scrollbar-color, content-visibility, same-document view transitions, invoker commandsBaseline 2025
@scope, container style queries, field-sizing, contrast-color(), sibling-index()Baseline 2026 (newly available)
Anchor positioningcore in all engines; MDN: limited
text-wrap: pretty, scroll-driven animations, cross-document view transitionsChromium + Safari
if(), interpolate-size, calc-size(), scroll-state queries, corner-shape, reading-flowChromium only

Guard newer features with @supports:

@supports (anchor-name: --a) { /* anchored menu */ }
@supports not (field-sizing: content) {
  textarea { min-block-size: 6lh; }
}
@supports selector(:has(a)) { /* :has styles */ }

Recipes

Centering

Four ways, depending on the parent's layout.

centering.html
<style>
  /* grid: one child, both axes */
  .center { display: grid; place-items: center; }
 
  /* flex: both axes, several children in a row */
  .row-center { display: flex;
    justify-content: center; align-items: center; }
 
  /* block container, vertical only (Baseline 2024) */
  .v-center { align-content: center; }
 
  .demo { display: grid; gap: 8px;
    grid-template-columns: repeat(3, 1fr); }
  .demo > div { height: 100px; border-radius: 4px;
    border: 1px dashed var(--muted); }
  b { padding: 4px 8px; margin: 2px; border-radius: 4px;
    background: var(--graph-0); color: var(--bg); }
</style>
<div class="demo">
  <div class="center"><b>grid</b></div>
  <div class="row-center"><b>flex</b><b>2</b></div>
  <div class="v-center"><b>block</b></div>
</div>
Result
/* horizontal block with a max width */
.wrap {
  inline-size: min(100% - 2rem, 70rem);
  margin-inline: auto;
}

A header, sidebar, main and footer that collapse to one column on narrow screens.

.app {
  display: grid;
  min-block-size: 100dvh;
  grid-template:
    "head" auto "main" 1fr "side" auto "foot" auto / 1fr;
}
@media (width >= 56rem) {
  .app {
    grid-template:
      "head head" auto
      "side main" 1fr
      "foot foot" auto / 16rem minmax(0, 1fr);
  }
}
.app > header { grid-area: head; }
.app > aside  { grid-area: side; }
.app > main   { grid-area: main; }
.app > footer { grid-area: foot; }

Responsive card grid

As many columns as fit, with no media queries and no overflow on tiny screens. The demo uses a smaller --min so several columns fit in the frame; resize the window to see them reflow.

card-grid.html
<style>
  .grid {
    --min: 16rem;
    display: grid;
    grid-template-columns: repeat(
      auto-fit, minmax(min(100%, var(--min)), 1fr)
    );
    gap: clamp(1rem, 2vw, 2rem);
  }
 
  .grid { --min: 9rem; }   /* demo only */
  .grid > div { padding: 12px; border-radius: 6px;
    background: var(--chip); }
</style>
<div class="grid">
  <div>1</div><div>2</div><div>3</div>
  <div>4</div><div>5</div>
</div>
Result

Fluid type scale

Sizes that grow smoothly from a 360px to a 1280px viewport.

:root {
  --step--1: clamp(0.83rem, 0.79rem + 0.19vw, 0.94rem);
  --step-0: clamp(1rem, 0.95rem + 0.22vw, 1.13rem);
  --step-1: clamp(1.2rem, 1.12rem + 0.37vw, 1.41rem);
  --step-2: clamp(1.44rem, 1.31rem + 0.56vw, 1.76rem);
  --step-3: clamp(1.73rem, 1.55rem + 0.82vw, 2.2rem);
  --step-4: clamp(2.07rem, 1.8rem + 1.19vw, 2.75rem);
}
body { font-size: var(--step-0); }
h1 { font-size: var(--step-4); text-wrap: balance; }
h2 { font-size: var(--step-3); text-wrap: balance; }

Dark mode with light-dark()

Follows the OS by default; data-theme on html forces one mode.

:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
 
:root {
  --bg: light-dark(oklch(99% 0 0), oklch(17% 0.01 260));
  --fg: light-dark(oklch(22% 0.02 260), oklch(93% 0 0));
  --line: light-dark(oklch(90% 0 0), oklch(30% 0.01 260));
}
body { background: var(--bg); color: var(--fg); }
type Theme = "light" | "dark" | "system";
 
export function setTheme(theme: Theme): void {
  const root = document.documentElement;
  if (theme === "system") delete root.dataset.theme;
  else root.dataset.theme = theme;
  localStorage.setItem("theme", theme);
}

Visually hidden

Hidden on screen but still read by screen readers; the second rule shows skip links on focus.

.visually-hidden:not(:focus-within, :active) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

Truncate and line clamp

Single-line ellipsis, or clamp to N lines.

truncate.html
<style>
  .truncate {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;             /* when inside flex or grid */
  }
 
  .clamp-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
  }
 
  p { max-width: 280px; margin: 0 0 12px;
    padding-inline: 6px; background: var(--chip); }
</style>
<p class="truncate">One line that is far too long to fit
  in its box, so it ends in an ellipsis.</p>
<p class="clamp-3">A paragraph that keeps going for several
  lines. Line clamping stops it after the third line and
  puts the ellipsis there, however much text follows in
  the markup, so card previews line up.</p>
Result

Full-bleed layout

Readable content column, with chosen children breaking out to the viewport edge.

.content {
  --gutter: 1rem;
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [content-start] min(65ch, 100% - 2 * var(--gutter))
    [content-end] minmax(var(--gutter), 1fr) [full-end];
}
.content > * { grid-column: content; }
.content > .full-bleed { grid-column: full; }

References