TocTable of Contents

Css limited features

Overview of css features with limited availability

::column

The ::column pseudo-element allows you to style individual columns in a CSS multi-column layout. Previously, there was no way to apply backgrounds, borders, or other styles to column boxes themselves — you could only style the multi-column container as a whole, or elements placed within it.

1
2
3
4
5
6
7
8
9
.article {
  columns: 3;
}

.article::column {
  background: #f9f9f9;
  border-right: 1px solid #e0e0e0;
  padding: 1rem;
}

The set of properties available on ::column is limited to those that make sense for a column fragment box — primarily backgrounds, borders, padding, and similar box-decoration properties. Properties that affect layout or flow are not applicable.

This fills a gap in multi-column styling that previously required workarounds such as negative margins, wrapper elements, or JavaScript-measured positioning to achieve column-level visual treatment.

::selection

The ::selection pseudo-element applies styles to the portion of a document that has been highlighted by the user — typically by clicking and dragging with a mouse or selecting text with a keyboard. It allows the default browser selection highlight to be replaced with custom colors.

1
2
3
4
5
6
7
8
9
::selection {
  background-color: oklch(70% 0.2 250);
  color: white;
}

.code-block::selection {
  background-color: oklch(80% 0.15 130);
  color: #1a1a1a;
}

Only a limited set of properties are supported on ::selection: color, background-color, text-decoration and its longhands, text-shadow, and stroke-color. The background shorthand is not supported — use background-color explicitly.

Custom selection colors are a subtle but effective branding touch, particularly on editorial or portfolio sites. It is important to maintain sufficient contrast between the selection background and text color to keep selected text readable — the same accessibility considerations that apply to normal text apply here.

::selection matches the active selection. Inactive selections (when the window loses focus) are not separately targetable in CSS and typically revert to a greyed-out system default.

::spelling-error and ::grammar-error

The ::spelling-error and ::grammar-error pseudo-elements let you style the text that a browser or OS spell-checker has flagged, giving you control over how error highlights appear rather than leaving it entirely to the user agent’s default rendering (typically a red or green wavy underline).

1
2
3
4
5
6
7
8
9
::spelling-error {
  text-decoration: underline wavy #e53e3e;
  color: inherit;
}

::grammar-error {
  text-decoration: underline wavy #d69e2e;
  color: inherit;
}

The properties you can apply to these pseudo-elements are limited to a subset of CSS — color, background, cursor, caret, outline, and text decoration properties — reflecting that the browser controls which ranges of text are marked, while CSS only controls their visual treatment.

This is most useful when your application has a specific design language and the browser’s default error styling clashes with it, or when you want to differentiate spelling and grammar errors more clearly than the defaults do.

:has-slotted

The :has-slotted pseudo-class matches a <slot> element when it has been assigned slotted content — that is, when the slot has been filled by content from the light DOM. This allows web component authors to style slot elements differently depending on whether they contain content or are empty.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Inside a web component's shadow DOM stylesheet */

slot:not(:has-slotted) {
  display: none; /* Hide empty slots */
}

.card__footer slot:has-slotted {
  padding-top: 1rem;
  border-top: 1px solid var(--border-color);
}

slot[name="icon"]:has-slotted ~ .label {
  padding-left: 0.5rem;
}

Previously, detecting whether a slot was filled required JavaScript using slot.assignedNodes(), and conditionally applying classes or styles based on the result. :has-slotted brings this entirely into CSS, making it straightforward to conditionally show wrapper elements, adjust spacing, or shift layout based on whether optional slot content has been provided.

:host-context()

The :host-context() pseudo-class, used inside a shadow DOM stylesheet, matches the shadow host element when any of its ancestors in the light DOM match the given selector. It allows a web component to adapt its styles based on the context in which it is placed, without requiring the consumer to pass that context in via attributes or custom properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Inside a web component's shadow DOM */

/* Style differently when inside a dark-themed container */
:host-context(.dark-theme) {
  --bg: #1a1a1a;
  --text: #f0f0f0;
}

/* Adjust layout when inside a sidebar */
:host-context(aside) {
  display: block;
  font-size: 0.875rem;
}

/* Respond to a data attribute on any ancestor */
:host-context([dir="rtl"]) {
  text-align: right;
}

:host-context(selector) walks up the DOM tree from the shadow host through all its light DOM ancestors, matching if any of them satisfy the selector. This is particularly useful for theming — a component can automatically adopt a dark or high-contrast style when placed inside an appropriately marked container, without any direct communication between the container and the component.

Note that :host-context() is considered at risk in the CSS specification and its future standardisation is uncertain. Browser support is reasonably good but it is not fully standardised, and CSS custom properties or explicit attribute-based theming (matched with :host([theme="dark"])) are more robust alternatives for production component theming.

:open

The :open pseudo-class matches interactive elements that are currently in an open or expanded state. It provides a unified selector for elements whose open state is toggled by the browser, covering <details>, <dialog>, <select>, <input> types with pickers, and elements with the popover attribute.

Previously, different elements required different selectors to target their open state: details[open] for details elements, no standard selector for open dialogs, and so on. :open unifies these under a single pseudo-class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
details:open summary {
  font-weight: bold;
}

dialog:open {
  animation: fade-in 0.2s ease;
}

[popover]:open {
  opacity: 1;
  transform: translateY(0);
}

select:open {
  border-color: var(--color-focus);
}

Using :open makes styles more readable and consistent, and reduces the need to remember which attribute or state mechanism each element type uses for its open condition.

:target-within

The :target-within pseudo-class matches an element when it or any of its descendants matches :target — that is, when the element contains the element whose id matches the current URL fragment. It is the target equivalent of :focus-within, propagating the targeted state up the DOM tree.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.section:target-within {
  background: oklch(97% 0.02 250);
  outline: 2px solid var(--color-primary);
  border-radius: 4px;
}

.card-list:target-within .card:not(:target) {
  opacity: 0.5; /* Dim non-targeted cards when one is targeted */
}

nav:target-within {
  /* Highlight the nav when a nav item is targeted */
  border-bottom: 2px solid var(--color-accent);
}

Where :target matches only the element with the matching id, :target-within allows ancestor elements to respond to a descendant being targeted. This makes it possible to highlight a containing section, card, or sidebar when a user navigates to an anchor link within it.

A practical example is a FAQ list where each question has an anchor — :target-within on the <li> would highlight the entire question-and-answer pair when the question’s anchor is targeted, rather than just the heading element itself.

As of early 2026, :target-within was defined in the CSS Selectors Level 4 specification but had no browser support. It is a future-facing feature that mirrors the pattern established by :focus-within.

@function

The @function at-rule allows you to define reusable custom functions in CSS. A custom function takes parameters, performs calculations, and returns a value — similar to a function in a preprocessor like Sass, but built into the browser and reactive to the cascade.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@function --fluid-size(--min, --max, --min-width: 320px, --max-width: 1200px) {
  result: clamp(
    var(--min),
    calc(var(--min) + (var(--max) - var(--min)) *
      ((100vw - var(--min-width)) / (var(--max-width) - var(--min-width)))),
    var(--max)
  );
}

h1 {
  font-size: --fluid-size(1.5rem, 3rem);
}

p {
  font-size: --fluid-size(1rem, 1.25rem);
}

Custom functions are defined with a name starting with --, accept typed or untyped parameters with optional defaults, and return a value via the result declaration. They participate in the cascade like any other CSS declaration and can reference other custom properties.

@function reduces repetition in complex CSS calculations and makes intent clearer than deeply nested calc() expressions, without requiring a build step or preprocessor.

alignment-baseline

The alignment-baseline property specifies which baseline of an inline or SVG element is aligned to the dominant baseline of its parent. It determines the vertical alignment point used when placing the element within a line box, based on typographic baselines rather than the element’s edges.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tspan {
  alignment-baseline: alphabetic;
}

.math-symbol {
  alignment-baseline: mathematical;
}

.cjk-inline {
  alignment-baseline: ideographic;
}

text > tspan {
  alignment-baseline: middle;
}

The available values correspond to typographic baseline positions: alphabetic (the Latin text baseline), ideographic (the baseline used for CJK ideographs), mathematical (the baseline for mathematical notation), central, middle, text-before-edge, text-after-edge, before-edge, and after-edge.

alignment-baseline is primarily a property for SVG text layout, where precise control over how inline elements and text spans align relative to the parent text element is needed. In HTML contexts, vertical-align handles most analogous use cases, though alignment-baseline maps more directly to the underlying font metric system. It works in conjunction with dominant-baseline, which establishes the baseline of the parent against which child elements are aligned.

Alternative style sheets

Alternative style sheets allow a page to offer multiple named stylesheets that the user can switch between. They are defined using <link> elements in HTML with a rel="alternate stylesheet" attribute and a title attribute identifying the style set. The default stylesheet uses rel="stylesheet" with a title.

1
2
3
4
5
6
7
8
9
<!-- Persistent stylesheet (always active) -->
<link rel="stylesheet" href="base.css">

<!-- Default preferred stylesheet -->
<link rel="stylesheet" title="Default" href="default.css">

<!-- Alternative stylesheets -->
<link rel="alternate stylesheet" title="High contrast" href="high-contrast.css">
<link rel="alternate stylesheet" title="Large text" href="large-text.css">

A stylesheet with a title but without alternate is the preferred stylesheet — active by default. Alternative stylesheets are inactive until selected. Persistent stylesheets (no title) are always applied regardless of which alternative is active.

Some browsers — historically Firefox — exposed a built-in UI for switching between alternative stylesheets via the View menu. This native browser support is inconsistent and largely absent from modern browsers, so switching is typically implemented with JavaScript that sets sheet.disabled on the appropriate <link> elements.

Alternative style sheets represent an early web platform feature for user-selectable themes. The same goals are more commonly achieved today with CSS custom properties, prefers-color-scheme media queries, and JavaScript-driven class or attribute toggling on the root element.

at-rule()

The at-rule() function is used inside @supports conditions to test whether the browser supports a specific CSS at-rule. This extends @supports beyond property-value pairs to enable feature detection of at-rules like @layer, @property, @font-face descriptors, and others.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@supports at-rule(@property) {
  /* Only applies if @property is supported */
  @property --my-color {
    syntax: '<color>';
    inherits: false;
    initial-value: blue;
  }

  .element {
    --my-color: red;
    color: var(--my-color);
  }
}

@supports at-rule(@layer) {
  /* Styles that depend on cascade layers being available */
  @layer base {
    body { margin: 0; }
  }
}

Without at-rule(), there was no way to query whether a particular at-rule was understood by the browser in CSS — you could only test properties and values, or selectors with the selector() function. at-rule() fills that gap for at-rule feature detection.

This is particularly useful for @property, which enables typed and animatable custom properties — a significantly different capability from basic custom properties — and where a fallback to untyped custom properties may be appropriate for non-supporting browsers.

As of early 2026, at-rule() support in @supports was still limited across browsers and should be used with awareness of its availability.

attr() (content only)

The attr() function retrieves the value of an HTML attribute from the element and makes it available as a string in CSS. In its baseline form — widely supported across all browsers — it works only inside the content property of ::before and ::after pseudo-elements, where it inserts the attribute value as generated text content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
a[href]::after {
  content: ' (' attr(href) ')';
}

abbr[title]::after {
  content: ' [' attr(title) ']';
}

[data-label]::before {
  content: attr(data-label);
}

This makes it possible to surface HTML attribute values visually in the rendered page without duplicating them in the markup or reaching for JavaScript. A common use case is print stylesheets that append URLs after links, since the href is not otherwise visible in print.

A more advanced form of attr() — which allows the function to be used in any CSS property and supports typed values like lengths, colors, and numbers — is defined in the specification but has limited browser support. The content-only form described here is the reliably available baseline that works in all modern browsers.

background-clip: border-area

The border-area value for background-clip restricts the background to paint only within the border itself, excluding the padding and content areas. This is distinct from the existing border-box value, which paints the background across the entire border box (content + padding + border), with the border drawn on top.

border-area makes it straightforward to apply a gradient or image directly to the border region — an effect that was previously achievable only through layered backgrounds, pseudo-elements, or border-image.

1
2
3
4
5
.fancy-border {
  border: 8px solid transparent;
  background-clip: border-area;
  background-image: linear-gradient(135deg, #f093fb, #f5576c);
}

A common pattern pairs border-area with a separate background for the content area, using multiple background-clip layers:

1
2
3
4
5
6
.gradient-border {
  border: 4px solid transparent;
  background:
    linear-gradient(white, white) padding-box,
    linear-gradient(to right, #6ee7f7, #a78bfa) border-area;
}

This renders a white content area with a gradient border, cleanly and without pseudo-elements.

background-clip: text

background-clip: text clips an element’s background to the shape of its text characters, creating gradient or image-filled typography entirely in CSS.

1
2
3
4
5
6
h1 {
  background: linear-gradient(135deg, #f093fb, #f5576c);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

color: transparent reveals the clipped background through the text.

Background-clip example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Animated Gradient */
@keyframes shift {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}

.animated-heading {
  background: linear-gradient(90deg, #4facfe, #00f2fe, #4facfe);
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shift 3s linear infinite;
}

/* With images */
.textured {
  background-image: url("texture.jpg");
  background-size: cover;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

Always include the prefix, -webkit-background-clip: text is still required alongside the standard property for complete browser coverage.

baseline-shift

The baseline-shift property moves an inline element up or down relative to the dominant baseline of its parent. It is the CSS equivalent of the SVG baseline-shift presentation attribute, and is primarily used in SVG text to produce superscript and subscript effects without changing font size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tspan.superscript {
  baseline-shift: super;
}

tspan.subscript {
  baseline-shift: sub;
}

tspan.raised {
  baseline-shift: 0.4em;
}

tspan.lowered {
  baseline-shift: -0.3em;
}

The super and sub keywords shift the element to a typical superscript or subscript position relative to the parent’s baseline. Length and percentage values provide precise control over the shift amount — positive values move the element up, negative values move it down.

In HTML, vertical-align with super and sub values, or with explicit length offsets, serves the same purpose for inline elements. baseline-shift is specifically relevant when authoring SVG text or working in contexts where SVG’s typographic model applies. The two properties address the same visual intent through different specifications: vertical-align for HTML inline layout, baseline-shift for SVG text layout.

calc-size()

calc-size() extends the capabilities of calc() to work with intrinsic size keywords like auto, min-content, max-content, and fit-content — values that calc() cannot handle because they are not fixed lengths.

This opens up arithmetic operations involving intrinsic sizes, which was previously impossible in CSS without JavaScript measurement. It also plays a key role in enabling transitions and animations to and from intrinsic sizes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.panel {
  width: calc-size(max-content, size + 2rem);
}

.expandable {
  height: 0;
  transition: height 0.3s ease;

  &.open {
    height: calc-size(auto, size);
  }
}

In the transition example, calc-size(auto, size) resolves to the element’s natural auto height, allowing a smooth animation that was not achievable before without JavaScript to measure and set explicit pixel values. calc-size() works alongside interpolate-size to make this kind of intrinsic-size animation possible declaratively.

Case-sensitive attribute selector

The s flag in CSS attribute selectors enforces case-sensitive matching of attribute values. In HTML, attribute values are matched case-insensitively by default for certain attributes, but the s flag overrides this to require an exact case match.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Only matches type="text", not type="TEXT" or type="Text" */
input[type="text" s] {
  border: 1px solid #ccc;
}

/* Only matches the exact data attribute value */
[data-status="Active" s] {
  color: green;
}

/* Only matches href ending in exactly ".PDF" */
a[href$=".PDF" s] {
  /* Would not match .pdf or .Pdf */
}

The s flag is placed inside the brackets after the value, separated by whitespace — the same position as the i (case-insensitive) flag. The two flags are mutually exclusive.

In HTML documents, attribute value matching is case-insensitive for certain well-known attributes by default — type, rel, for, and others. The s flag forces case-sensitive matching even for these, while for attributes that are already case-sensitive by default (such as data-* attributes and most others in HTML), the s flag makes the case sensitivity explicit without changing behaviour.

Case-sensitive matching is most relevant in XML and SVG contexts where attribute values are inherently case-sensitive, or in HTML where you specifically need to distinguish between data-status="active" and data-status="Active" as distinct states.

color-contrast()

The color-contrast() function selects the colour from a list that has the highest contrast ratio against a specified background colour. It automates the common task of choosing between a dark or light foreground colour to ensure readable text over a dynamic background.

1
2
3
4
5
.badge {
  --bg: var(--brand-color);
  background: var(--bg);
  color: color-contrast(var(--bg) vs white, black);
}

The syntax specifies a base colour, the keyword vs, and then a comma-separated list of candidate colours to compare against it. The function returns whichever candidate achieves the highest contrast ratio with the base.

1
2
3
4
5
6
7
8
9
.button {
  background: var(--accent);
  color: color-contrast(
    var(--accent) vs
    white,
    #1a1a1a,
    var(--color-text-muted)
  );
}

An optional target contrast level can be added using to, which causes the function to return the first candidate that meets or exceeds the specified level rather than simply the highest contrast option:

1
2
3
.label {
  color: color-contrast(var(--bg) vs white, black to AA);
}

The AA and AAA keywords correspond to the WCAG 2.1 contrast ratio thresholds of 4.5:1 and 7:1 respectively. A specific ratio can also be provided as a number.

color-contrast() is closely related to contrast-color(), which is a simpler function returning whichever of black or white contrasts best with a given colour. color-contrast() is the more powerful version, accepting an arbitrary list of candidates and optional WCAG targets, though browser support is still limited and it is currently behind flags in most browsers.

Container scroll-state queries

Scroll-state container queries extend the @container query syntax to allow styling based on the scroll state of a scroll container. Rather than querying size or style, these queries respond to whether a container is currently scrolled, stuck (as a sticky element), or snapped (as a scroll snap target).

A container must be declared as a scroll-state container before it can be queried:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.scroller {
  container-type: scroll-state;
  overflow-y: auto;
}

@container scroll-state(scrollable: top) {
  .scroll-hint {
    opacity: 0;
  }
}

@container scroll-state(scrollable: bottom) {
  .load-more {
    display: block;
  }
}

Sticky element state can also be queried, removing the need for IntersectionObserver hacks to detect when a sticky header is actively stuck:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.sticky-header {
  container-type: scroll-state;
  position: sticky;
  top: 0;
}

@container scroll-state(stuck: top) {
  .sticky-header {
    box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
  }
}

Scroll-state queries bring a class of UI behaviours — scroll-aware components, sticky state styling, snap position detection — into CSS without JavaScript event listeners.

Container style queries

Container style queries let you apply styles based on the computed value of a CSS custom property on a container element — not just its size. This enables state-driven styling through CSS alone.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.card-wrapper {
  container-name: card;
  --variant: featured;
}

@container card style(--variant: featured) {
  .card {
    border: 2px solid gold;
    background: #fffbeb;
  }
}

When the container has --variant: featured, the card gets special styling.

Style queries can be used for theming

1
2
3
4
5
6
7
8
.theme-wrapper {
  container-name: theme;
  --color-scheme: dark;
}

@container theme style(--color-scheme: dark) {
  .component { background: #1a1a2e; color: #e0e0e0; }
}

Quite powerful, because previously, changing styles based on a parent’s state required JavaScript adding classes. Style queries allow this with pure CSS custom properties.

Container style queries can be combined with size queries

1
2
3
@container sidebar (width < 200px) and style(--compact: true) {
  .nav-item { padding: 0.25rem; }
}

contrast-color()

The contrast-color() function returns the color from a provided list that has the highest contrast against a given background color. Its initial, simplified form takes a single background color and returns whichever of white or black contrasts best with it.

1
2
3
4
5
.badge {
  --bg: var(--brand-color);
  background: var(--bg);
  color: contrast-color(var(--bg));
}

This eliminates a very common problem in dynamic theming: ensuring text remains readable over a background whose color is set at runtime via a custom property or user preference. Previously this required JavaScript to calculate luminance and decide between dark and light text. contrast-color() does this in CSS, reacting automatically to changes in the background value.

The longer-term specification extends the function to accept an arbitrary list of candidate colors, returning whichever achieves the best contrast ratio, along with optional target contrast level constraints. The simpler contrast-color(color) form returning black or white is the initial implementation available in browsers.

CSS object model (DOM level 2)

The DOM Level 2 CSS specification defined the original programmatic interface for accessing and manipulating CSS stylesheets — the interfaces that underpin document.styleSheets, CSSStyleSheet, CSSRuleList, and CSSStyleRule. While still functional and widely used, parts of this API are considered legacy and some patterns it enabled are discouraged in modern development.

The most common discouraged pattern is directly modifying stylesheet rules via CSSStyleSheet.insertRule() and deleteRule() for general styling tasks, or accessing element.style for computed values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Discouraged: accessing element.style for computed values
// element.style only reflects inline styles, not computed styles
const color = element.style.color; // May be empty even if color is set by stylesheet

// Correct: use getComputedStyle for resolved values
const color = window.getComputedStyle(element).color;

// Discouraged for dynamic theming: manipulating stylesheet rules directly
document.styleSheets[0].insertRule('.theme { color: red }', 0);

// Preferred: use CSS custom properties instead
document.documentElement.style.setProperty('--theme-color', 'red');

The DOM Level 2 API also uses string-based property access (cssRule.style.setProperty('color', 'red')) which lacks type safety. The CSS Typed Object Model (Houdini) provides a more modern, type-safe alternative via element.attributeStyleMap and element.computedStyleMap().

For reading computed styles, getComputedStyle() remains the standard approach. For dynamic styling, CSS custom properties manipulated via style.setProperty() are strongly preferred over direct stylesheet manipulation.

CSS typed object model

The CSS Typed Object Model (Typed OM) is a JavaScript API that exposes CSS property values as typed objects rather than plain strings, making programmatic CSS manipulation faster, safer, and easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* reading values */
const el = document.querySelector('.box');
const styles = el.computedStyleMap();

const width = styles.get('width');
console.log(width.value); // 200
console.log(width.unit);  // "px"
 
/* writing values */
el.attributeStyleMap.set('width', CSS.px(300));
el.attributeStyleMap.set('opacity', CSS.number(0.5));
el.attributeStyleMap.set('transform', new CSSTranslate(CSS.px(10), CSS.px(20)));

CSS Factory Functions

1
2
3
4
CSS.px(10)        // CSSUnitValue { value: 10, unit: 'px' }
CSS.percent(50)   // CSSUnitValue { value: 50, unit: 'percent' }
CSS.deg(45)       // CSSUnitValue { value: 45, unit: 'deg' }
CSS.number(1.5)   // CSSUnitValue { value: 1.5, unit: 'number' }

Why this is better than getComputedStyle

  • No string parsing — values come as typed objects with .value and .unit
  • Math is straightforward: width.value + 10 instead of parseFloat(width) + 10
  • Errors are caught early with meaningful messages

Cursor styles

The cursor property sets the type of cursor shown when the mouse pointer is over an element. It communicates affordance — what the user can do with the element — and is an important part of interactive UI design.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.button {
  cursor: pointer; /* Hand cursor, signals clickability */
}

.disabled {
  cursor: not-allowed;
}

.draggable {
  cursor: grab;
}

.draggable:active {
  cursor: grabbing;
}

.resizable {
  cursor: ew-resize; /* East-west resize arrow */
}

.text-area {
  cursor: text;
}

The most commonly used keyword values include default (the standard arrow), pointer (pointing hand for links and buttons), text (I-beam for text), move (four-directional arrow), grab and grabbing (for draggable elements), not-allowed (blocked action), wait (loading), crosshair, and a full set of directional resize cursors (n-resize, ew-resize, nwse-resize, etc.).

Custom cursors can be specified using url(), with fallback keywords:

1
2
3
.custom {
  cursor: url('cursor.png') 8 8, pointer;
}

The two numbers after the URL are the cursor’s hotspot coordinates — the pixel offset of the active click point from the top-left of the image. A fallback keyword is required after any url() value.

cursor: none hides the cursor entirely, which is occasionally used in games or immersive experiences that render their own custom cursor with JavaScript.

Device media queries

Device media queries target characteristics of the output device or environment beyond viewport dimensions — things like screen resolution, display color capability, pointer precision, and user preferences. They complement width-based responsive breakpoints by allowing styles to adapt to the nature of the device itself rather than just its size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* High-resolution / retina displays */
@media (resolution >= 2dppx) {
  .logo {
    background-image: url('logo@2x.png');
  }
}

/* Devices with a precise pointer (mouse) */
@media (pointer: fine) {
  .button {
    padding: 0.5rem 1rem;
  }
}

/* Devices with a coarse pointer (touch) */
@media (pointer: coarse) {
  .button {
    padding: 0.75rem 1.25rem; /* Larger touch target */
  }
}

/* Devices that support hover */
@media (hover: hover) {
  .card:hover {
    transform: translateY(-2px);
  }
}

/* color display capability */
@media (color) {
  /* Device supports color */
}

@media (monochrome) {
  /* Black and white display */
}

The prefers-* media features query user preferences set at the OS level:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@media (prefers-color-scheme: dark) {
  :root { color-scheme: dark; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

@media (prefers-contrast: more) {
  :root {
    --color-text: black;
    --color-bg: white;
  }
}

@media (prefers-reduced-data: reduce) {
  .hero-video { display: none; }
}

Device queries are particularly important for accessibility — prefers-reduced-motion should be respected in any site that uses animation, and prefers-contrast allows high-contrast mode users to receive appropriately adjusted styles.

display animation

display is now animatable as a discrete property, meaning it can participate in CSS transitions and keyframe animations when transition-behavior: allow-discrete is set. Previously, display toggled instantly between values with no interpolation, making it impossible to animate elements in or out while simultaneously hiding them from the layout.

With allow-discrete, the browser handles the timing of the discrete switch intelligently: when transitioning to display: none, the switch happens at the end of the transition (after other properties like opacity have finished animating); when transitioning from display: none, the switch happens at the start (so the element is visible while other properties animate in).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.toast {
  display: block;
  opacity: 1;
  transition: opacity 0.3s ease, display 0.3s ease;
  transition-behavior: allow-discrete;

  @starting-style {
    opacity: 0;
  }
}

.toast.hidden {
  display: none;
  opacity: 0;
}

This pattern, combined with @starting-style for the enter state, provides complete enter and exit animations on display-toggled elements using only CSS. The same approach works with content-visibility and overlay for popover and dialog animations.

display-mode media query

The display-mode media feature detects how a web application is currently being displayed — as a regular browser tab, a standalone installed PWA, a fullscreen app, or in a minimal browser UI. It allows CSS to adapt the interface based on the launch context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Default browser tab */
@media (display-mode: browser) {
  .install-prompt {
    display: block;
  }
}

/* Installed as a standalone PWA */
@media (display-mode: standalone) {
  .install-prompt {
    display: none; /* Already installed, hide the prompt */
  }

  .nav-bar {
    padding-top: env(safe-area-inset-top); /* Account for notch */
  }
}

/* Fullscreen mode */
@media (display-mode: fullscreen) {
  .exit-fullscreen-button {
    display: block;
  }
}

/* Minimal browser UI */
@media (display-mode: minimal-ui) {
  .custom-back-button {
    display: flex;
  }
}

The values correspond to the display field in a web app manifest: browser (normal tab), minimal-ui (browser with minimal UI controls), standalone (app-like, no browser chrome), and fullscreen (no browser UI at all).

display-mode is particularly useful in progressive web apps to hide browser-specific UI elements (like install prompts or navigation chrome) when the app is running in standalone or fullscreen mode, and to adjust layout for the absence of browser navigation controls.

display: contents

display: contents causes an element to generate no box of its own. The element itself is not rendered, but its children are treated as if they were direct children of the element’s parent in the layout. The element effectively disappears from the box tree while remaining in the DOM.

1
2
3
4
5
.wrapper {
  display: contents;
  /* This element generates no box — its children participate
     directly in the parent's layout context */
}

A common use case is making a semantically useful wrapper element invisible to the layout system. For example, a <fieldset> or <ul> might be needed for semantic or accessibility reasons, but its default box model interferes with a grid or flex layout on the parent. Setting display: contents removes the wrapper from the layout without removing it from the document.

1
2
3
4
5
6
7
8
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.grid-section {
  display: contents; /* Children of this element become direct grid items */
}
1
2
3
4
5
6
7
<div class="grid">
  <div class="grid-section">
    <div>Item 1</div>
    <div>Item 2</div>
  </div>
  <div>Item 3</div>
</div>

There is an important accessibility caveat: some browsers incorrectly remove elements with display: contents from the accessibility tree entirely, meaning screen readers may not announce the element or its semantic role. This makes it unsuitable for elements whose semantics matter — such as <button>, <a>, <fieldset>, or heading elements — until browser support improves. For purely presentational wrapper <div> elements it is generally safe.

field-sizing

The field-sizing property controls whether form fields size themselves based on their content or maintain a fixed size. The default behaviour for text inputs and textareas is field-sizing: fixed — they have a defined size and do not grow or shrink as the user types. Setting field-sizing: content changes this so the field adjusts its size to fit its current value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
textarea {
  field-sizing: content;
  min-height: 3lh;
  max-height: 20lh;
}

input[type="text"] {
  field-sizing: content;
  min-width: 10ch;
}

With field-sizing: content, a textarea grows as the user types more content, up to any max-height you set, and a text input expands horizontally to fit its value. Previously, this behaviour required JavaScript event listeners on the input to manually adjust dimensions.

Combining field-sizing: content with min-* and max-* properties gives you full control over the range of sizes the field can take, keeping the experience predictable while still being responsive to the user’s content.

filter()

The CSS filter property applies graphical effects — blur, brightness, contrast, color shifts, shadows, and more — to an element and all of its contents. Filter functions can be chained, and each is applied in sequence to the output of the previous.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.blurred {
  filter: blur(4px);
}

.dark-overlay {
  filter: brightness(0.6);
}

.grayscale-image {
  filter: grayscale(100%);
}

.vintage-photo {
  filter: sepia(60%) contrast(1.2) saturate(0.8);
}

.drop-shadow {
  filter: drop-shadow(4px 4px 8px rgb(0 0 0 / 0.3));
}

The available filter functions are blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), and sepia().

filter: drop-shadow() is distinct from box-shadow — it follows the actual shape of the element including transparent areas, making it ideal for PNG images and SVG elements where a shadow should trace the content’s outline rather than its rectangular box.

Applying filter creates a new stacking context and, for most filter types, triggers GPU compositing. This is both a performance benefit for animated filters and a potential source of z-index surprises. filter: none removes all filters.

backdrop-filter applies the same filter functions to the area behind an element rather than the element itself, enabling the frosted glass effect.

fit-content()

The fit-content() function is used as a track sizing function in CSS Grid, setting a track to grow to fit its content up to a specified maximum size. It is equivalent to min(max-content, max(min-content, argument)) — the track sizes to its content naturally, but is clamped at the provided maximum.

1
2
3
.grid {
  grid-template-columns: fit-content(200px) 1fr fit-content(300px);
}

In this example, the first and third columns size to their content but will not grow beyond 200px and 300px respectively. The middle column takes up all remaining space with 1fr.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.sidebar-layout {
  display: grid;
  grid-template-columns: fit-content(250px) 1fr;
}

.label-grid {
  display: grid;
  grid-template-columns: fit-content(120px) auto;
  gap: 0.5rem 1rem;
}

fit-content() is distinct from the fit-content keyword (without parentheses), which is used as a width or height value on regular elements. The function form is specifically for grid track sizing and requires an argument defining the maximum size the track may reach.

This makes it ideal for sidebar columns, label columns in form layouts, and any grid track that should shrink-wrap its content but be bounded to prevent it from becoming too wide when content is long.

font-language-override

The font-language-override property controls which language-specific glyph variants a font uses, independently of the document’s lang attribute. Many OpenType fonts include alternate glyphs optimised for particular languages — the same Unicode character may be rendered differently in Serbian versus Russian, or in Traditional versus Simplified Chinese.

Normally, the browser selects these variants based on the element’s language as set by lang. font-language-override lets you specify a different OpenType language system tag directly, overriding that automatic selection.

1
2
3
4
5
6
7
8
.serbian-text {
  /* Use Serbian-specific glyph variants regardless of document lang */
  font-language-override: 'SRB';
}

.romanian-text {
  font-language-override: 'ROM';
}

The value is an OpenType language system tag — a four-character string defined by the OpenType specification. The default value normal defers to the language inferred from the lang attribute.

This property is most useful when document-level language tagging does not match the language of a specific block of text, or when you need to mix glyph sets from different language variants within a single document.

font-palette-animation

The font-palette property selects a color palette from a color font’s built-in palette definitions. color fonts (in formats like COLRv1) contain multiple named or indexed palettes that define how their built-in colors are applied. font-palette lets you switch between them or define custom overrides using @font-palette-values.

What makes this particularly powerful is that font-palette is animatable. You can transition smoothly between two palettes — or between a palette and a custom color override — creating animated color effects in multi-color fonts without any JavaScript or SVG tricks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@font-palette-values --pale {
  font-family: 'MyColorFont';
  override-colors: 0 #e8d5c0, 1 #c4a882;
}

@font-palette-values --vivid {
  font-family: 'MyColorFont';
  override-colors: 0 #ff4500, 1 #ff8c00;
}

.logo {
  font-palette: --pale;
  transition: font-palette 0.4s ease;
}

.logo:hover {
  font-palette: --vivid;
}

The browser interpolates between the individual color values in the palettes, producing a smooth cross-fade of the font’s colors on hover, focus, or any other state change.

font-synthesis-position

font-synthesis-position controls whether the browser synthesises superscript and subscript positioning when the font does not include dedicated superior or inferior glyphs.

When using font-variant-position: super or sub, browsers ideally use specially designed smaller glyphs positioned at the correct height. Without them, the browser scales down regular glyphs and repositions them — faux positioning.

1
2
3
:root {
  font-synthesis-position: none;
}

With synthesis disabled, font-variant-position: super or sub has no effect if the font lacks true positional glyphs — preventing poorly scaled substitutes.

1
font-synthesis-position: auto;

This is a longhand of the font-synthesis shorthand:

1
font-synthesis: none; /* disables weight, style, small-caps, AND position */

font-variant-position uses designed glyphs at appropriate sizes. vertical-align: super/sub simply shifts regular-sized text, which looks too large for footnote markers or chemical formulas.

font-variant-emoji

The font-variant-emoji property controls whether emoji characters are rendered in their colorful emoji presentation or as plain text symbols. Unicode defines variation selectors that influence this (VS15 for text, VS16 for emoji), but font-variant-emoji gives you CSS-level control over the default presentation when no variation selector is present in the markup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.monochrome-icons {
  font-variant-emoji: text;
}

.colorful-labels {
  font-variant-emoji: emoji;
}

.system-default {
  font-variant-emoji: normal;
}

The text value renders the character as a monochrome text glyph, while emoji requests the full-color emoji rendering. unicode defers to the character’s default presentation as defined by Unicode, and normal leaves the decision to the browser and font.

This is useful when you want to use emoji characters as icon-like elements that blend with surrounding text styling, or conversely when you want to ensure colorful rendering in contexts where the text presentation might otherwise be selected.

font-variant-position

The font-variant-position property renders text as superscript or subscript using dedicated glyphs from the font, rather than the scaled and repositioned approach used by the HTML <sup> and <sub> elements. When a font includes purpose-designed superior and inferior figures and letters, they are optically better than synthesised alternatives.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.footnote-marker {
  font-variant-position: super;
}

.chemical-formula sub {
  font-variant-position: sub;
}

.reset {
  font-variant-position: normal;
}

The super value uses OpenType superior glyphs (sups feature) — smaller letterforms drawn specifically for superscript use, with strokes and proportions optimised at their intended small size. The sub value uses inferior glyphs (subs feature) for subscripts. These are distinct from scaled-down regular glyphs, which appear thin and spindly at small sizes.

If the font does not include superior or inferior glyphs, the browser may fall back to synthesised positioning or simply render the text normally. font-synthesis: position controls whether synthesis is permitted when the actual glyphs are unavailable.

The difference from vertical-align: super and vertical-align: sub is that font-variant-position uses glyphs designed for the purpose, while vertical-align repositions regular-sized or font-size-reduced characters — which can disrupt line spacing when the repositioned characters extend beyond the line box.

font-width

The font-width property selects a font face based on its width — how condensed or expanded the letterforms are. It is the standardised longhand for what font-stretch has represented, providing a cleaner name that better reflects what is being controlled.

For variable fonts that include a width axis (wdth), font-width can be set to any percentage value within the font’s supported range. For non-variable fonts, the browser selects the closest available face.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.condensed-label {
  font-width: 75%; /* Condensed */
}

.expanded-headline {
  font-width: 125%; /* Expanded */
}

.normal-body {
  font-width: normal; /* 100% */
}

Named keywords are also accepted: ultra-condensed (50%), extra-condensed (62.5%), condensed (75%), semi-condensed (87.5%), normal (100%), semi-expanded (112.5%), expanded (125%), extra-expanded (150%), and ultra-expanded (200%).

font-width is particularly useful with variable fonts in editorial or display contexts where fine control over the horizontal density of text contributes to the typographic character of the design.

Gap decorations

Gap decorations are a set of CSS properties that allow visual styling to be applied directly to the gaps between items in flex, grid, and multi-column layouts. Rather than using borders, pseudo-elements, or background tricks to simulate dividers, gap decoration properties paint directly into the gap area.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
  gap-decoration: 1px solid #e0e0e0;
}

.flex-row {
  display: flex;
  gap: 1.5rem;
  column-gap-decoration: 1px dashed #ccc;
}

The gap decoration properties follow a similar model to border — they accept a width, style, and colour. The shorthand gap-decoration sets the decoration for all gaps, while row-gap-decoration and column-gap-decoration target horizontal and vertical gaps independently.

1
2
3
4
5
6
7
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1px;
  gap-decoration: 1px solid var(--color-border);
  gap-decoration-break: intersecting; /* Controls how decorations meet at intersections */
}

Gap decorations resolve a long-standing CSS pain point — drawing dividers between grid or flex items without resorting to border on each item (which creates double borders at shared edges) or complex :not(:last-child) selector hacks. This feature is part of the CSS Box Alignment specification and was still gaining browser support as of early 2026.

glyph-orientation-vertical

The glyph-orientation-vertical property was an SVG presentation attribute and early CSS property that controlled the orientation of glyphs when text was set in a vertical writing mode. It specified the angle at which characters were rotated within a vertical text run.

1
2
3
4
5
6
7
8
9
/* Legacy SVG usage */
text {
  writing-mode: tb;
  glyph-orientation-vertical: 0; /* Upright */
}

text.rotated {
  glyph-orientation-vertical: 90; /* Rotated 90 degrees */
}

The property accepted angle values in degrees (without a unit), where 0 rendered glyphs upright and 90 rotated them sideways — the typical orientation for Latin text set within a vertical CJK text run.

glyph-orientation-vertical is now obsolete and has been replaced by the text-orientation property, which handles glyph orientation in vertical text more cleanly and as part of the standardised CSS Writing Modes specification.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* Modern equivalent */
.vertical-text {
  writing-mode: vertical-rl;
  text-orientation: mixed;    /* CJK upright, Latin rotated */
}

.all-upright {
  writing-mode: vertical-rl;
  text-orientation: upright;  /* All characters upright */
}

text-orientation: mixed is the standard default for vertical text — it keeps CJK characters upright while rotating Latin and other scripts 90 degrees. For new projects, text-orientation should be used instead of glyph-orientation-vertical.

Heading pseudo-classes

CSS does not have dedicated heading pseudo-classes, but the :is() pseudo-class provides a concise way to target multiple heading levels in a single selector, avoiding repetitive comma-separated rules. This is the modern idiomatic approach to styling headings as a group.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Without :is() — verbose */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  line-height: 1.2;
}

/* With :is() — concise */
:is(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-display);
  line-height: 1.2;
}

/* Contextual heading selectors */
article :is(h2, h3, h4) {
  margin-top: 2rem;
  margin-bottom: 0.5rem;
}

:is(h1, h2, h3, h4, h5, h6) + p {
  margin-top: 0; /* Remove top margin from paragraph immediately after a heading */
}

The specificity of :is() is determined by its most specific argument — :is(h1, h2, h3) has the same specificity as a single type selector (h1).

:where() is a zero-specificity equivalent of :is(), useful when you want heading group styles to be easily overridden:

1
2
3
4
:where(h1, h2, h3, h4, h5, h6) {
  /* Zero specificity — any later rule can override without needing higher specificity */
  text-wrap: balance;
}

Together, :is() and :where() make heading group selectors both concise and appropriately manageable in the cascade.

Hyphenate limit chars

hyphenate-limit-chars sets the minimum number of characters required before a word can be hyphenated, and the minimum number of characters that must remain before and after the hyphen. This prevents awkward very-short hyphenations.

1
2
3
4
5
p {
  hyphens: auto;
  hyphenate-limit-chars: 6 3 2;
  /* min-word  min-before  min-after */
}
  • 6 — word must be at least 6 characters to be hyphenated
  • 3 — at least 3 characters before the hyphen
  • 2 — at least 2 characters after the hyphen
1
2
3
4
hyphenate-limit-chars: auto;       /* browser default */
hyphenate-limit-chars: 6;          /* min-word only, browser chooses before/after */
hyphenate-limit-chars: 6 3;        /* min-word and min-before */
hyphenate-limit-chars: 6 3 2;      /* all three */

Without limits, short words like “in” or “to” might be hyphenated unnecessarily. Setting a reasonable minimum word length (5–7) prevents this.

if()

The if() function brings conditional logic directly into CSS property values. It evaluates one or more conditions and returns a different value depending on which condition is true, without requiring separate rules or selectors.

1
2
3
4
5
6
7
.button {
  background: if(
    style(--variant: primary): var(--color-primary);
    style(--variant: danger): var(--color-danger);
    else: var(--color-secondary)
  );
}

Conditions can test style queries (custom property values), media features, or supports queries. The else branch provides a fallback when no condition matches.

1
2
3
4
5
6
7
.layout {
  grid-template-columns: if(
    media(width >= 1024px): repeat(3, 1fr);
    media(width >= 640px): repeat(2, 1fr);
    else: 1fr
  );
}

if() is particularly powerful in component design systems where a single element needs to vary across multiple states or themes based on custom property values, reducing the number of separate rules needed and keeping variant logic co-located with the property it affects.

ime-mode

The ime-mode property was intended to control the state of the Input Method Editor (IME) for text input fields. IMEs are the software systems that allow users to compose characters from languages such as Chinese, Japanese, and Korean using standard keyboards. The property could theoretically activate, deactivate, or disable the IME for a given input.

1
2
3
4
5
6
7
input.latin-only {
  ime-mode: disabled;
}

input.cjk-preferred {
  ime-mode: active;
}

The available values were auto (default, no change to IME state), normal (normal IME state), active (IME enabled on focus), inactive (IME disabled on focus), and disabled (IME completely disabled for the field).

ime-mode is now considered obsolete and should not be used in new projects. Browser support was always inconsistent — it was primarily implemented in Firefox and Internet Explorer, with limited or no support in other browsers. Modern browsers have removed or deprecated it.

The property’s goal of controlling input method behaviour for specific fields was never well served by a CSS property, as input method control is more appropriately handled at the application or OS level. Where language-specific input constraints are needed, HTML attributes such as inputmode and lang, combined with appropriate validation, are the recommended approach.

initial-letter

initial-letter creates drop caps and raised caps — enlarged first letters of a paragraph that span multiple lines — using pure CSS with proper typographic alignment.

1
2
3
p::first-letter {
  initial-letter: 3;
}

The first letter spans 3 lines of text, sinking into the paragraph.

1
2
3
p::first-letter {
  initial-letter: 3 1; /* size  sink */
}

sink: 1 raises the cap to sit on the first baseline rather than sinking — a “raised cap”.

1
2
3
4
5
6
p::first-letter {
  initial-letter: 4;
  font-family: "Playfair Display", serif;
  color: #c0392b;
  margin-right: 0.1em;
}

Simply enlarging the first letter with font-size does not handle baseline alignment across multiple lines. initial-letter positions the glyph correctly.

interpolate-size

The interpolate-size property opts an element into animating between a fixed length and an intrinsic size keyword such as auto, min-content, or max-content. By default, CSS cannot interpolate to or from these keywords because they are not fixed values — they resolve differently depending on content. interpolate-size: allow-keywords lifts that restriction.

The most common use case is animating an element’s height from 0 to auto, which was previously impossible in CSS without JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
:root {
  interpolate-size: allow-keywords;
}

.drawer {
  height: 0;
  overflow: hidden;
  transition: height 0.3s ease;
}

.drawer.open {
  height: auto;
}

Setting interpolate-size: allow-keywords on :root enables the behaviour globally, so all elements on the page can animate to and from intrinsic sizes. It can also be scoped to individual elements if a global opt-in is not desirable.

interpolate-size works in concert with calc-size() and transition-behavior to form the full set of tools for intrinsic-size animation in CSS.

inverted-colors media query

The inverted-colors media feature detects whether the operating system or browser has enabled a color inversion mode — such as macOS’s “Invert Colors” accessibility option or iOS Smart Invert. When active, the OS inverts all colors on screen, which can cause problems for certain types of content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@media (inverted-colors: inverted) {
  img,
  video {
    filter: invert(1); /* Re-invert images so they look correct */
  }

  .qr-code {
    filter: invert(1); /* Re-invert to restore black-on-white */
  }
}

The two values are none (no inversion active, the default) and inverted (the OS is inverting colors).

The primary use case is restoring images, videos, and other visual content to their original appearance when OS-level color inversion would otherwise make them look wrong. Photographs and illustrations look unusual when inverted — re-inverting them with filter: invert(1) cancels out the OS inversion and restores the original colors.

QR codes and barcodes are another important case — inverted versions may not be readable by scanners. Maps, diagrams, and branded illustrations may also need to be re-inverted to remain meaningful.

inverted-colors is an accessibility-oriented media feature with limited but growing browser support. It is most relevant on Apple platforms where color inversion is a common accessibility setting.

justify-self-block

justify-self now works in block layout, not just grid and absolute positioning. Applied to a block-level element, it controls that element’s alignment along the inline axis — horizontally in left-to-right writing modes — within its containing block.

Previously, horizontally aligning a block element within its parent meant using margin: auto tricks or switching to flexbox or grid just for alignment purposes. justify-self makes this a direct, readable property on the element itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.container {
  width: 100%;
}

.content {
  width: 60ch;
  justify-self: center; /* Centre this block without margin: auto */
}

.aside {
  width: 20rem;
  justify-self: end; /* Push to the right edge */
}

Accepted values include start, end, center, stretch, and the same alignment keywords available in flex and grid contexts. Like align-content in block layouts, this reduces the number of situations where a full layout model change is needed just to control positioning.

line-clamp

Line clamping truncates text to a specified number of lines and appends an ellipsis at the point of truncation. The most widely supported approach uses a combination of prefixed and legacy properties, though a standardised line-clamp property is defined in the CSS Overflow specification.

The current cross-browser technique uses -webkit-line-clamp with a set of companion properties:

1
2
3
4
5
6
.clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

This limits the element’s visible content to three lines, with an ellipsis automatically appended at the cut-off point. Despite the -webkit- prefix, this technique works in all modern browsers including Firefox.

The standardised line-clamp property, defined in CSS Overflow Level 4, simplifies this to a single declaration and does not require the display or overflow companion properties:

1
2
3
.clamped {
  line-clamp: 3; /* Standardised — browser support still limited */
}

The standard property also supports a two-value syntax allowing a custom string to replace the ellipsis:

1
2
3
.clamped {
  line-clamp: 3 ' [more]';
}

Until the standardised property achieves full browser support, the -webkit-box approach remains the reliable production technique. The two can be layered so that browsers adopting the standard form will automatically use it while others fall back.

margin-trim

margin-trim instructs a container to discard the margins of its child elements at the container’s edges — eliminating the need for :first-child / :last-child hacks to remove unwanted space.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.container p { margin-block: 1rem; }
/* First and last <p> add unwanted space at container edges */

/* Solution before margin-trim */
.container p:first-child { margin-top: 0; }
.container p:last-child  { margin-bottom: 0; }

/* The new solution */
.container {
  margin-trim: block;
}

This automatically trims the margin-block-start of the first child and margin-block-end of the last child.

1
2
3
4
5
margin-trim: none;        /* default — no trimming */
margin-trim: block;       /* trim block-start and block-end edges */
margin-trim: inline;      /* trim inline-start and inline-end edges */
margin-trim: block-start; /* trim only the block-start edge */
margin-trim: block-end;   /* trim only the block-end edge */

How to use

  • Article containers where paragraphs have block margins
  • Card components with padded containers
  • Any component where edge margins cause visual inconsistency

Masonry

CSS masonry layout is a two-dimensional layout mode where items are placed into columns but pack upward into the available space left by shorter items in the row above — like bricks laid in a wall, or the Pinterest-style card grid. It eliminates the uneven gaps that appear in a standard grid when items have varying heights.

Masonry is defined as part of CSS Grid Layout and is enabled by setting one of the grid axes to masonry:

1
2
3
4
5
6
.masonry-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry;
  gap: 1rem;
}

With grid-template-rows: masonry, items are placed into columns defined by grid-template-columns, but the row sizing is handled by the masonry algorithm — each item sits as close to the top as possible, filling gaps left by shorter siblings.

The axis can be flipped for a horizontal masonry layout:

1
2
3
4
5
.horizontal-masonry {
  display: grid;
  grid-template-columns: masonry;
  grid-template-rows: repeat(3, 1fr);
}

Items can still be explicitly placed on the non-masonry axis using grid-column (for vertical masonry) and can span multiple columns.

CSS masonry was still in the experimental phase as of early 2026, available behind flags in some browsers. The feature has been actively debated — an alternative proposal using display: masonry was also under consideration — so the final syntax may differ from the current specification. JavaScript-based masonry libraries (such as Masonry.js) remain the production-ready alternative in the interim.

Numeric factory functions

CSS numeric factory functions — CSS.px(), CSS.em(), CSS.percent(), CSS.deg(), and others — are part of the CSS Typed OM API. They create typed CSSUnitValue objects for safe, readable programmatic CSS manipulation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
CSS.px(10)         // { value: 10, unit: 'px' }
CSS.em(1.5)        // { value: 1.5, unit: 'em' }
CSS.rem(2)         // { value: 2, unit: 'rem' }
CSS.percent(50)    // { value: 50, unit: 'percent' }
CSS.vw(100)        // { value: 100, unit: 'vw' }
CSS.vh(100)        // { value: 100, unit: 'vh' }
CSS.deg(45)        // { value: 45, unit: 'deg' }
CSS.turn(0.5)      // { value: 0.5, unit: 'turn' }
CSS.s(0.3)         // { value: 0.3, unit: 's' }
CSS.number(1.5)    // { value: 1.5, unit: 'number' }

/* Setting Styles */
const el = document.querySelector('.box');
el.attributeStyleMap.set('width', CSS.px(200));
el.attributeStyleMap.set('transform',
  new CSSTranslate(CSS.px(10), CSS.percent(50))
);

/* Arithmetic */
const width = CSS.px(100).add(CSS.px(50)); // CSS.px(150)

Why use these?

  • Type safety — mismatched units are caught at assignment
  • No string concatenation: CSS.px(val) vs val + "px"
  • Composable with other Typed OM objects like CSSTranslate

overflow-clip-margin

The overflow-clip-margin property controls how far outside an element’s border box content is allowed to paint before being clipped, when overflow: clip is set. It defines an outset from the element’s padding edge within which overflow is still permitted to render.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.clipped {
  overflow: clip;
  overflow-clip-margin: 20px; /* Allow 20px of overflow before clipping */
}

.tight {
  overflow: clip;
  overflow-clip-margin: 0; /* Default: clip exactly at the border edge */
}

.generous {
  overflow: clip;
  overflow-clip-margin: content-box 1rem;
}

overflow: clip is similar to overflow: hidden in that it prevents scrolling and clips content, but it does not establish a new block formatting context — making it more predictable in certain layout situations. overflow-clip-margin extends the clip boundary outward by the specified amount, giving content a margin of space to paint into before being cut off.

This is particularly useful for elements where a drop shadow, outline, or other ink overflow effect needs to bleed slightly outside the element’s box without establishing a full new stacking or formatting context. Without overflow-clip-margin, overflow: clip cuts content off sharply at the border edge, which can unintentionally hide box shadows or focus rings.

The value accepts a length, or a geometry box keyword (content-box, padding-box, border-box) followed by an optional length, specifying the reference box from which the outset is measured.

overflow: overlay

overflow: overlay displays scrollbars that float over the content rather than occupying layout space, preserving the element’s full width. It is now an alias for overflow: auto in most modern browsers.

1
2
3
.scrollable {
  overflow: overlay;
}

On systems with overlay scrollbars (macOS, iOS, some Linux), this produces scrollbars that appear on hover without shifting the layout.

overflow: overlay was a non-standard value introduced by WebKit. It has since been standardised as an alias for overflow: auto in the CSS spec — browsers that show overlay scrollbars will do so automatically with auto.

On macOS and mobile devices, overflow: auto already shows overlay scrollbars. For Windows-style persistent scrollbars, use the scrollbar-gutter property instead:

1
2
3
4
.scrollable {
  overflow: auto;
  scrollbar-gutter: stable; /* reserve space for scrollbar even when not scrolling */
}

overflow: overlay is deprecated. New projects should use overflow: auto and scrollbar-gutter for reliable cross-platform behaviour.

overlay

The overlay property controls whether an element is rendered in the browser’s “top layer” — the layer used for <dialog>, popovers, and fullscreen elements — and crucially, whether it stays there during exit transitions.

When a dialog closes, it is normally removed from the top layer immediately, cutting off any exit animation before it completes.

1
2
3
4
5
6
7
8
9
dialog {
  transition: opacity 0.3s, overlay 0.3s allow-discrete;
  opacity: 1;
}

dialog:not([open]) {
  opacity: 0;
  overlay: none;
}

overlay: allow-discrete keeps the element in the top layer until its transition finishes.

Full animated dialog dismissal uses three features together:

1
2
3
4
5
6
dialog {
  transition:
    display 0.3s allow-discrete,
    overlay 0.3s allow-discrete,
    opacity 0.3s;
}
  1. display — deferred so element stays visible during fade
  2. overlay — deferred so element stays on top during fade
  3. opacity — the actual visible animation

Page break aliases

The page-break-before, page-break-after, and page-break-inside properties are legacy aliases for the modern break-before, break-after, and break-inside properties. They were introduced for controlling page breaks in print layouts and are now superseded by the generalised fragmentation properties, which work across paged media, multi-column layouts, and CSS regions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Legacy syntax */
h2 {
  page-break-before: always;
}

.no-break {
  page-break-inside: avoid;
}

figure {
  page-break-after: avoid;
}

/* Modern equivalents */
h2 {
  break-before: page;
}

.no-break {
  break-inside: avoid;
}

figure {
  break-after: avoid;
}

The legacy properties accept values including auto, always, avoid, left, right, and page. The modern break-* properties accept these and additional values that apply to column and region contexts: column, avoid-column, avoid-page, and others.

Browsers continue to support the legacy properties for backwards compatibility, and they map directly to their break-* equivalents. For new print stylesheets, the modern break-before, break-after, and break-inside properties are preferred as they are more versatile and better reflect the unified fragmentation model.

Page selectors

Page selectors are pseudo-classes used within @page rules to target specific pages in a paged document — such as the first page, left-hand pages, right-hand pages, and blank pages. They allow different margin, size, and content settings to be applied per page type in print layouts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@page {
  margin: 2cm;
}

@page :first {
  margin-top: 4cm; /* Extra space on the title page */
}

@page :left {
  margin-left: 3cm;
  margin-right: 2cm;
}

@page :right {
  margin-left: 2cm;
  margin-right: 3cm;
}

@page :blank {
  @top-center {
    content: none; /* No header on intentionally blank pages */
  }
}

:first matches the first page of the document. :left and :right match left-hand and right-hand pages in a double-sided (spread) layout — which is :left or :right depends on the writing direction and the @page size configuration. :blank matches pages that are empty due to forced page breaks.

Named pages extend this system further — the page property on an element associates it with a named @page rule, allowing entirely different page configurations for different sections of a document:

1
2
3
4
5
6
7
@page chapter-start {
  margin-top: 6cm;
}

.chapter-heading {
  page: chapter-start;
}

Page selectors are primarily relevant for print stylesheets and PDF generation, where precise control over individual page types is a typographic requirement.

page-orientation

The page-orientation descriptor, used inside @page rules, controls the rotation of a printed page. It allows individual pages or page groups within a document to be oriented differently — for example, rotating a single wide table to landscape while the rest of the document remains portrait.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@page {
  size: A4 portrait;
}

@page landscape-page {
  size: A4 landscape;
  page-orientation: rotate-left;
}

.wide-table {
  page: landscape-page;
}

The rotate-left and rotate-right values rotate the page content accordingly, while upright is the default with no rotation applied. The page property on the element associates it with a named @page rule, so different parts of the document can use different page configurations.

page-orientation fills a long-standing gap in CSS print layout, where mixed portrait and landscape pages in a single document previously required separate files or complex workarounds.

paint()

The paint() function is part of the CSS Houdini CSS Painting API. It calls a custom Paint Worklet — a JavaScript class that implements the paint() method — to generate an image that can be used as a background, border image, or any other property that accepts an image value. It brings programmable, canvas-like drawing directly into CSS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* Registering the worklet */
CSS.paintWorklet.addModule('my-painter.js');

/* Using it in CSS */
.element {
  background: paint(my-painter);
}

.custom-border {
  border-image: paint(fancy-border) 30;
}

.with-params {
  --paint-color: oklch(60% 0.2 250);
  --paint-size: 8;
  background: paint(dots-painter);
}

The Paint Worklet script defines a class with a static inputProperties array (listing which CSS custom properties it reads) and a paint() method that receives a canvas-like rendering context, the element’s geometry, and its custom property values:

1
2
3
4
5
6
7
8
9
// my-painter.js
registerPaint('my-painter', class {
  static get inputProperties() { return ['--paint-color']; }

  paint(ctx, geom, properties) {
    ctx.fillStyle = properties.get('--paint-color').toString();
    ctx.fillRect(0, 0, geom.width, geom.height);
  }
});

Paint Worklets run off the main thread and re-execute whenever the element’s size or specified custom properties change, enabling responsive custom graphics without JavaScript event listeners. The API is part of CSS Houdini — browser support is currently limited to Chromium-based browsers.

prefers-reduced-data media query

The prefers-reduced-data media feature detects whether the user has requested that the browser use less data — for example, when they have enabled a data-saver mode in their operating system or browser, or when they are on a metered connection. It allows CSS to reduce the amount of data downloaded as part of rendering the page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@media (prefers-reduced-data: reduce) {
  .hero {
    background-image: none; /* Skip the large background photo */
    background-color: var(--color-primary);
  }

  .video-poster {
    display: none;
  }

  .webfont {
    font-family: system-ui, sans-serif; /* Fall back to system font */
  }
}

The two values are no-preference (the default, no data-saving request) and reduce (the user or their network environment has indicated a preference for reduced data usage).

Practical applications include skipping large background images, deferring non-critical web fonts in favour of system fonts, omitting decorative video or animation assets, and loading lower-resolution images. These changes can significantly reduce page weight for users on slow or metered connections.

As of early 2026, prefers-reduced-data had limited browser support and was not yet implemented in most major browsers without flags, though it is defined in the Media Queries Level 5 specification. It is worth implementing proactively so the styles are in place when support broadens.

prefers-reduced-transparency media query

The prefers-reduced-transparency media query detects whether the user has requested reduced transparency in their OS accessibility settings. Some users find frosted glass and translucent UI elements distracting or difficult to read.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@media (prefers-reduced-transparency: reduce) {
  .glass-panel {
    background: rgba(255, 255, 255, 0.95); /* nearly opaque */
    backdrop-filter: none;
  }

  .tooltip {
    background: #333;  /* solid fallback */
    opacity: 1;
  }
}
1
2
3
@media (prefers-reduced-transparency: no-preference) {
  /* User has not requested reduced transparency — use full effects */
}

When reduce is active:

  • Replace backdrop-filter: blur() with a solid or near-solid background
  • Increase opacity of translucent overlays
  • Simplify layered semi-transparent UI

Users with vestibular disorders, visual processing difficulties, or cognitive conditions may enable this setting. It is available on macOS (Accessibility → Display) and iOS.

progress()

The progress() function calculates where a value falls between a minimum and maximum, returning a number between 0 and 1 that represents its position in that range. It is the CSS equivalent of a linear normalisation calculation — the same operation that JavaScript developers often write as (value - min) / (max - min).

1
2
3
4
5
6
7
8
.element {
  --value: 650;
  --min: 320;
  --max: 1200;

  /* Returns a 0–1 ratio of where --value sits in the range */
  opacity: progress(var(--value), var(--min), var(--max));
}

The result can be used directly as a unitless value (for opacity, font-weight, etc.) or multiplied to produce values in any unit. Combined with @property for typed custom properties and calc(), progress() enables expressive range-based calculations directly in CSS.

It pairs naturally with scroll-driven animations, where you might want to express a style as a proportion of scroll progress, and with @function for building reusable interpolation utilities.

random()

The random() function generates a random value within a specified range, directly in CSS. It accepts a minimum and maximum value and returns a random number between them, optionally snapped to a given step interval.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.particle {
  width: random(4px, 20px);
  height: random(4px, 20px);
  opacity: random(0.3, 1);
}

.item {
  /* Random value snapped to 10px increments */
  margin-top: random(10px, 60px, by 10px);
}

By default, each element gets its own independently randomised value. A random-caching-key parameter can be used to share a random value across multiple elements or properties — useful when you want a group of elements to share the same random offset, or when you need the same random value applied to multiple properties of a single element.

1
2
3
4
5
.card {
  --r: random(0deg, 5deg, by 1deg, cache random-tilt);
  rotate: var(--r);
  outline-offset: calc(var(--r) * 2);
}

random() is a generative design tool built into CSS, enabling organic variation in layouts, animations, and visual treatments without JavaScript.

Range syntax for style queries

Style container queries now support range syntax for querying numeric custom property values, using comparison operators rather than equality checks. This mirrors the range syntax already available for size container queries and media queries, and allows you to apply styles when a custom property value falls within, above, or below a threshold.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@container style(--size > 2) {
  .label {
    font-size: 1.25rem;
  }
}

@container style(1 <= --level <= 3) {
  .indicator {
    background: var(--color-warning);
  }
}

@container style(--priority >= 5) {
  .item {
    font-weight: bold;
    border-left: 4px solid var(--color-urgent);
  }
}

Without range syntax, style queries could only check whether a custom property equalled a specific value. Range syntax makes it practical to build components that respond to a spectrum of values — priority levels, size scales, quantity thresholds — without requiring a separate query for each discrete value.

reading-flow

The reading-flow property controls the order in which focus and reading tools (such as screen readers and keyboard navigation) traverse the items in a flex or grid container, independently of their visual order. This addresses a long-standing accessibility concern where CSS layout properties like order, flex-direction: row-reverse, or grid placement can produce a visual order that differs from the DOM order — and therefore from the tab/focus order that assistive technologies follow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  reading-flow: grid-rows; /* Follow visual row order */
}

.flex-reversed {
  display: flex;
  flex-direction: row-reverse;
  reading-flow: flex-visual; /* Follow visual left-to-right order */
}

The grid-rows value makes focus follow the visual row-by-row order of grid items. grid-columns follows column order. flex-visual follows the visual order of flex items regardless of flex-direction or order. flex-flow follows the flex flow direction.

Without reading-flow, the only way to align focus order with a rearranged visual layout was to reorder the DOM itself — which has its own problems — or use tabindex on every item, which is fragile and verbose.

resize

The resize property controls whether an element can be resized by the user, and in which directions. By default, browsers make <textarea> elements resizable — resize allows this behaviour to be controlled or extended to other elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
textarea {
  resize: vertical; /* Only vertical resizing (default for textarea) */
}

.resizable-box {
  overflow: auto; /* Required for resize to work on non-textarea elements */
  resize: both;
  min-width: 200px;
  min-height: 100px;
}

.no-resize {
  resize: none;
}

.horizontal-only {
  overflow: hidden;
  resize: horizontal;
}

The accepted values are none, both, horizontal, and vertical. For resize to work on elements other than <textarea>, the element must have its overflow set to a value other than visible — typically auto, hidden, or scroll.

resize: none is commonly applied to textareas in designs where the default resize handle would break a layout, though removing it reduces usability for users who need more space to write. field-sizing: content is now a better alternative for textareas that should grow with their content automatically, removing the need for manual resizing entirely.

The resize handle appears in the element’s bottom-right corner by default and can be styled via the ::-webkit-resizer pseudo-element in WebKit-based browsers.

Rhythmic sizing

Rhythmic sizing is a set of CSS features designed to keep vertical spacing consistent with a typographic baseline grid — ensuring that the height of every element, including images, figures, and other non-text content, snaps to a multiple of the base line height. This preserves vertical rhythm across the entire page.

The core property is line-height-step, which causes block-level elements to round their block size up to the nearest multiple of the specified value:

1
2
3
4
5
6
7
8
:root {
  --line-height: 1.5rem;
  line-height: var(--line-height);
}

html {
  line-height-step: 1.5rem; /* Snap all block heights to multiples of 1.5rem */
}

With line-height-step set, any block element whose content does not exactly fill a whole number of line-height units will have padding added at the bottom to round up to the next step. This keeps baseline alignment consistent regardless of content.

1
2
3
4
figure {
  /* Image height will be rounded up to the nearest multiple of the line-height step */
  line-height-step: 1.5rem;
}

Rhythmic sizing works alongside the rlh unit, which represents the root element’s line height — making it straightforward to express spacings and sizes as multiples of the baseline without manual calculations.

As of early 2026, line-height-step had limited browser support and was not yet available in production use without workarounds. The feature is defined in the CSS Rhythmic Sizing specification. In the interim, careful use of rlh units and consistent spacing tokens approximates the same result.

sibling-count() and sibling-index()

sibling-count() and sibling-index() are CSS functions that return information about an element’s position among its siblings. sibling-index() returns the element’s one-based index within its parent, and sibling-count() returns the total number of siblings. Both return integers usable in calc() and other math contexts.

These functions unlock a category of dynamic styling that previously required either inline styles set by JavaScript, or elaborate :nth-child workarounds that only worked for a fixed number of items.

1
2
3
4
5
6
7
.item {
  /* Stagger animation delays based on position */
  animation-delay: calc(sibling-index() * 0.1s);

  /* Spread hue across all items */
  color: hsl(calc(sibling-index() / sibling-count() * 360deg) 70% 50%);
}
1
2
3
4
.grid {
  /* Adjust column count based on how many items there are */
  grid-template-columns: repeat(sibling-count(), 1fr);
}

The ability to reference an element’s position and the total count directly in CSS removes a whole class of JavaScript-assisted styling patterns, particularly for lists, card grids, animated sequences, and data visualisations.

speak

The speak property is a CSS aural property that controls whether an element’s content should be spoken aloud by a speech synthesiser. It was part of the CSS2 Aural Style Sheets specification, which defined a full set of properties for controlling speech output. The property has since been moved to the CSS Speech module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.screen-only {
  speak: none; /* Do not read this element aloud */
}

.read-aloud {
  speak: normal; /* Default: read content aloud */
}

.spell-out {
  speak: spell-out; /* Read content letter by letter */
}

normal is the default — the element’s content is read using normal language rules. none suppresses speech output entirely for the element, similar to display: none for visual rendering. spell-out causes the content to be spelled out character by character, which is sometimes useful for codes, abbreviations, or strings where the default pronunciation would be incorrect.

The speak property should be distinguished from the speak-as descriptor used in @counter-style rules, which controls how counter values are pronounced by screen readers.

In practice, most of the CSS aural properties including speak have poor support in mainstream browsers, as they were primarily designed for dedicated speech browsers and assistive technology environments. For accessibility control over what screen readers announce, ARIA attributes — particularly aria-hidden, aria-label, and aria-live — are the reliable and widely supported alternative.

speak-as

The speak-as descriptor is used inside @counter-style rules to define how a custom counter style should be spoken aloud by speech synthesis systems — primarily screen readers. It maps the visual counter representation to an appropriate spoken form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@counter-style thumbs {
  system: cyclic;
  symbols: '\1F44D';
  suffix: ' ';
  speak-as: bullets; /* Speak as a generic list item, not as the emoji name */
}

@counter-style roman-upper {
  system: additive;
  additive-symbols: 1000 M, 900 CM, 500 D, 400 CD, 100 C, 90 XC,
                    50 L, 40 XL, 10 X, 9 IX, 5 V, 4 IV, 1 I;
  speak-as: numbers; /* Speak the numeric value rather than the Roman numeral */
}

@counter-style custom-alpha {
  system: alphabetic;
  symbols: A B C D E F;
  speak-as: auto; /* Default: infer from the counter system */
}

The accepted values are auto (the default, which infers appropriate speech from the counter system), bullets (treated as an unordered list item), numbers (the numeric value is spoken), words (the symbol is spoken as a word), spell-out (the symbol is spelled out letter by letter), and a <counter-style-name> reference to another counter style whose speech rules should be used.

speak-as is relevant for accessibility in documents that use CSS counter styles for list markers or generated content, ensuring that the spoken representation is meaningful even when the visual symbol — an emoji, a custom glyph, or an obscure alphabetic system — would not be naturally readable by a speech engine.

stretch

stretch is a standardised keyword for sizing properties like width, height, min-width, and max-width. It makes an element size itself to fill the available space in its containing block, equivalent to how a block element behaves horizontally by default — but applicable to any axis and any element type.

Previously, this behaviour was only achievable via the vendor-prefixed -webkit-fill-available or -moz-available, which were inconsistent across browsers and not part of any standard.

1
2
3
4
5
6
7
8
.button {
  width: stretch; /* Fill the available inline space */
}

.panel {
  height: stretch; /* Fill available block space */
  min-height: stretch;
}

stretch is particularly useful for inline elements or replaced elements (like <button> and <input>) that do not naturally fill their container width, and for elements in flex or grid contexts where you want an item to expand without using flex: 1 or align-self: stretch.

It can also be used in fit-content alternatives and alongside min-width and max-width to set a responsive size range anchored to the available space.

text-box

The text-box property is a shorthand for text-box-trim and text-box-edge, which together control the trimming of the space above and below text within its line box. By default, text in CSS carries extra space above capital letters (between the top of the em square and the cap height) and below the baseline (for descenders). This built-in spacing makes precise vertical alignment and optical centering difficult.

text-box lets you trim this space, so the measured box of a text element corresponds to the actual visual ink bounds of the letters rather than the theoretical em square.

1
2
3
4
5
6
7
8
9
.button {
  /* Trim space above cap height and below baseline */
  text-box: trim-both cap alphabetic;
  padding: 1rem 1.5rem;
}

h1 {
  text-box: trim-start cap;
}

The first value after trim-both / trim-start / trim-end is the over edge (top) metric — cap for cap height, ex for x-height, text for the full ascender. The second value is the under edge (bottom) metric — alphabetic for the baseline (excluding descenders), text for the full descender.

This makes it significantly easier to achieve optically centered text in buttons, badges, and headings without compensatory padding hacks, and to align text precisely with adjacent elements like icons.

The blink value for text-decoration-line was intended to make text flash on and off, similar to the behaviour of the old <blink> HTML element. It is defined in the CSS specification but browsers are explicitly permitted to ignore it — and all modern browsers do exactly that, rendering the text without any blinking effect.

1
2
3
.blinking {
  text-decoration-line: blink; /* Ignored by all modern browsers */
}

The blink value was deprecated because blinking text is widely considered a serious accessibility problem. It can trigger seizures in users with photosensitive epilepsy and is extremely distracting for users with attention or cognitive disabilities. The Web Content Accessibility Guidelines (WCAG) prohibit content that flashes more than three times per second.

The valid and useful values of text-decoration-line are none, underline, overline, and line-through, which can be combined:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.underline {
  text-decoration-line: underline;
}

.strikethrough {
  text-decoration-line: line-through;
}

.overline-and-underline {
  text-decoration-line: overline underline;
}

text-decoration-line is one of the longhands of the text-decoration shorthand, alongside text-decoration-color, text-decoration-style, and text-decoration-thickness.

text-spacing-trim

The text-spacing-trim property controls the trimming of intrinsic spacing built into CJK (Chinese, Japanese, Korean) punctuation characters. Many CJK punctuation glyphs include whitespace as part of their glyph box — for example, an ideographic full stop has space on one or both sides to maintain rhythm in CJK text. When mixed with other punctuation or at the start and end of lines, this built-in spacing can create excessive gaps.

text-spacing-trim lets you remove this spacing in a typographically appropriate way, following the rules of East Asian typography standards.

1
2
3
4
5
6
7
article {
  text-spacing-trim: trim-start;
}

.cjk-body {
  text-spacing-trim: space-all;
}

The trim-start value removes the spacing at the start of lines, which is a common requirement for CJK body text. space-all preserves all spacing, while trim-all removes it throughout. The feature works alongside text-autospace (which handles spacing between CJK and non-CJK characters) to give fine-grained typographic control over mixed-script and CJK-primary text.

text-wrap: pretty

text-wrap: pretty optimises line breaking to avoid leaving a single word orphaned on the last line of a paragraph, improving typographic quality in body text without the performance cost of full balancing.

1
2
3
p {
  text-wrap: pretty;
}

What is the difference from text-wrap: balance

  • balance — makes all lines equal length; capped at ~6 lines; good for headings
  • pretty — avoids orphans on the last line; works on any length of text; good for body copy
1
2
h1, h2, h3 { text-wrap: balance; }  /* headings */
p           { text-wrap: pretty; }  /* paragraphs */

text-wrap: pretty is less expensive than balance because it only optimises the last line rather than recalculating all line breaks. It is safe to apply to body text.

An orphan is a single word (or very short last line) at the end of a paragraph — common in justified text or narrow containers:

“The quick brown fox jumps over the lazy dog.”

pretty avoids this by reflowing the preceding line.

Time-relative pseudo-selectors

Time-relative pseudo-classes match elements based on their position relative to the current playback point in a timed media presentation — such as a WebVTT caption track or a Synchronized Multimedia Integration Language (SMIL) document. They provide a CSS hook for styling content that is currently active, past, or upcoming in a time-based sequence.

The three pseudo-classes are :current, :past, and :future.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Currently active caption */
:current(p) {
  background: rgba(0, 0, 0, 0.75);
  color: white;
}

/* Already spoken captions */
:past(p) {
  opacity: 0.5;
  color: #aaa;
}

/* Upcoming captions */
:future(p) {
  opacity: 0.3;
}

:current matches the element or elements that are currently being presented — for example, the active cue in a subtitle track. It can take a selector argument to narrow which elements it targets. :past matches elements whose time range has already passed. :future matches elements yet to be presented.

These pseudo-classes are defined in the CSS Selectors specification with their primary use case being WebVTT cue styling in <track> elements. Browser support is limited and they are primarily relevant in specialist media player contexts rather than general web development.

user-select

The user-select property controls whether the user can select text in an element. It is commonly used to prevent accidental text selection on interactive UI elements, or to make certain content non-selectable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.button {
  user-select: none; /* Clicking doesn't select button text */
}

.draggable {
  user-select: none; /* Dragging doesn't select text */
}

.code-block {
  user-select: all; /* One click selects the entire block */
}

.normal-text {
  user-select: text; /* Default: text is selectable */
}

none prevents the element’s text from being selected. text (the default) allows normal text selection. all makes the entire element’s content select as a unit on a single click — useful for code snippets, URLs, or any content that is typically copied in full. contain (formerly element in some browsers) confines selection to within the element’s bounds.

user-select: none does not prevent copying via keyboard shortcuts if the element is focused, and it does not affect programmatic selection via JavaScript (window.getSelection()). It only affects mouse and touch-based selection.

The property was long prefixed as -webkit-user-select for WebKit and Blink browsers. The unprefixed version is now widely supported, but for maximum compatibility with older browsers the prefixed form is sometimes still included alongside the standard property.

Widows and orphans

The widows and orphans properties control how paragraph text is split across page breaks, column breaks, or region breaks in paged and multi-column layouts. They specify the minimum number of lines that must remain together at the top or bottom of a fragment.

An orphan is one or more lines left alone at the bottom of a page before a break. A widow is one or more lines left alone at the top of a page after a break. Both are considered typographically poor and are avoided in quality print design.

1
2
3
4
p {
  orphans: 3; /* At least 3 lines must remain at the bottom before a break */
  widows: 3;  /* At least 3 lines must appear at the top after a break */
}

When a paragraph would be split in a way that leaves fewer lines than the specified minimum, the browser moves the break point to keep the required number of lines together, pushing more content to the next page or column.

Both properties accept integer values and default to 2. They are inherited, so setting them on body or a container applies throughout the document. Their effect is most apparent in print stylesheets and CSS multi-column layouts — in standard single-column screen layouts there are no page breaks, so the properties have no visual impact.

Note that widows and orphans address line-level breaks within paragraphs. The related break-inside, break-before, and break-after properties control breaks at the element level.

word-break: auto-phrase

word-break: auto-phrase instructs the browser to break lines at natural phrase boundaries rather than at arbitrary character positions, improving readability in CJK and mixed-script text.

1
2
3
p {
  word-break: auto-phrase;
}

Standard word breaking in CJK text can split grammatical phrases arbitrarily. auto-phrase uses linguistic analysis to identify natural pause points and break lines there, much like a human typesetter would.

Without auto-phrase:

東京都は日本の首都です。

With auto-phrase (breaks at natural phrase boundary):

東京都は日本の首都です。

The primary benefit today is for Japanese text. The browser’s line-breaking dictionary determines what counts as a phrase boundary.

1
2
3
4
word-break: normal;      /* default */
word-break: break-all;   /* break anywhere */
word-break: keep-all;    /* no mid-word breaks for CJK */
word-break: auto-phrase; /* break at natural phrases */

word-break: break-word

The word-break property controls how words break when they overflow their container. The break-word value — now better expressed as overflow-wrap: break-word — forces long unbreakable strings such as URLs, long technical identifiers, or strings without natural break points to wrap rather than overflow.

1
2
3
4
5
6
7
8
.user-content {
  word-break: break-word; /* Legacy value, widely supported */
}

/* Modern equivalent, preferred */
.user-content {
  overflow-wrap: break-word;
}

word-break: break-word was a non-standard value originally introduced by Internet Explorer and subsequently adopted across browsers for compatibility. It behaves identically to overflow-wrap: break-word — breaking words only when they would otherwise overflow, rather than breaking all words aggressively. The overflow-wrap property is the standardised version of what was historically also called word-wrap.

The standard word-break values are normal (default), break-all (breaks at any character, not just at overflow, which affects all words not just long ones), and keep-all (prevents breaks in CJK text). break-word was never in the specification as a value for word-break, though browsers honour it as an alias.

For new code, overflow-wrap: break-word is preferred. For maximum compatibility with older browsers in legacy codebases, both are sometimes declared together:

1
2
3
4
.safe {
  word-break: break-word;    /* Legacy */
  overflow-wrap: break-word; /* Standard */
}

writing-mode SVG 1.1 values

SVG 1.1 defined its own set of writing-mode values — lr, lr-tb, rl, rl-tb, tb, and tb-rl — which predate the CSS Writing Modes specification and used different naming conventions. These values were adopted into CSS for SVG compatibility but are now considered legacy and deprecated in favour of the standard CSS values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* SVG 1.1 legacy values (deprecated) */
text {
  writing-mode: tb-rl; /* Top to bottom, right to left — now: vertical-rl */
}

text {
  writing-mode: lr-tb; /* Left to right, top to bottom — now: horizontal-tb */
}

/* Modern CSS equivalents */
text {
  writing-mode: vertical-rl;
}

text {
  writing-mode: horizontal-tb;
}

The mapping between legacy and modern values is straightforward: lr and lr-tb map to horizontal-tb; rl and rl-tb also map to horizontal-tb (with direction: rtl for right-to-left); tb and tb-rl map to vertical-rl.

Modern browsers continue to support the legacy SVG values for backwards compatibility, but they should not be used in new code. The CSS Writing Modes values (horizontal-tb, vertical-rl, vertical-lr, sideways-rl, sideways-lr) are the standard and work consistently in both HTML and SVG contexts.

When authoring inline SVG within HTML documents, the CSS writing mode properties apply and the modern values should be used.

Background attachment

background-attachment controls whether a background image scrolls with the element’s contents, stays fixed relative to the viewport, or moves with the element’s local scroll container.

1
2
3
background-attachment: scroll; /* default — moves with the element */
background-attachment: fixed;  /* stays fixed to the viewport */
background-attachment: local;  /* moves with the element's own scroll */
1
2
3
4
5
6
7
8
/* Parallax effect with position: fixed */
.hero {
  background-image: url("landscape.jpg");
  background-attachment: fixed;
  background-size: cover;
  background-position: center;
  height: 80vh;
}

As the user scrolls, the content moves but the background stays put — creating a classic parallax effect.

1
2
3
4
5
/* Background scrolls with the div's own overflow scroll */
.scrollable { background-attachment: local; }

/* Background stays fixed within the div even when content scrolls */
.fixed-bg   { background-attachment: scroll; }

background-attachment: fixed forces the browser to repaint the background on every scroll, which can be expensive. For smoother parallax effects, use transform or the Intersection Observer API instead.

COLRv1

COLRv1 is a font format that enables color fonts with gradients, compositing, and detailed multi-layer glyphs — think rich emoji and icon fonts — rendered scalably at any size.

COLRv0 (the original COLR format) only supported flat color fills per glyph layer. COLRv1 adds:

  • Linear, radial, and sweep gradients
  • Compositing and blending modes
  • Variable color fonts (combining color with font variation axes)

No special CSS is required — use color fonts like any other:

1
2
3
body {
  font-family: "Noto Color Emoji", sans-serif;
}
1
2
3
4
5
6
7
/* Customising with font-palette */
@font-palette-values --custom {
  font-family: "Bungee Color";
  override-colors: 0 #ff4500;
}

h1 { font-palette: --custom; }

COLRv1 fonts are smaller than SVG or bitmap emoji fonts, render crisply at any size, support system-level accessibility color overrides, and integrate naturally with CSS forced-colors.

Column breaks

Column break properties let you control how content flows across columns in a multi-column layout — preventing awkward breaks inside headings or figures, or forcing new columns to begin at specific points.

1
2
3
4
5
6
7
h2, h3 {
  break-inside: avoid;
}

figure {
  break-inside: avoid-column;
}

avoid prevents a break inside the element. avoid-column is more specific to multi-column contexts.

1
2
3
.chapter-start {
  break-before: column;
}

Break Values

ValueEffect
autoNo forced break
avoidAvoid breaking inside
columnForce a column break
avoid-columnAvoid a column break specifically
pageForce a page break (print)

column-break-before, column-break-after, and column-break-inside are older equivalents. The current spec uses the generic break-before, break-after, and break-inside.

object-view-box

object-view-box defines a viewport into an image or video (a “replaced element”), letting you pan and crop without JavaScript — similar to SVG’s viewBox attribute.

1
2
3
img {
  object-view-box: inset(10% 20% 10% 20%);
}

This crops 10% from top and bottom and 20% from left and right of the image.

inset() follows the same shorthand as margin:

1
2
object-view-box: inset(top right bottom left);
object-view-box: inset(10px 20px);  /* vertical horizontal */
1
2
3
4
5
6
img {
  object-fit: cover;
  object-view-box: inset(20% 0% 20% 0%); /* crop top and bottom */
  width: 300px;
  height: 200px;
}

object-view-box is animatable, enabling Ken Burns-style pan and zoom effects in pure CSS.

Reversed counter-reset

The reversed() function in counter-reset creates a counter that counts down from the total number of items to 1, useful for reverse-numbered lists.

1
2
3
ol {
  counter-reset: reversed(list-item);
}

The list-item counter is the built-in counter used by ordered lists. Wrapping it in reversed() makes it count from the total down to 1.

1
2
3
4
5
6
7
8
9
ol {
  counter-reset: reversed(my-counter);
  list-style: none;
}

li::before {
  counter-increment: my-counter;
  content: counter(my-counter) ". ";
}

HTML has a reversed attribute on <ol> that does the same thing:

1
2
3
4
5
<ol reversed>
  <li>Last item</li>
  <li>Middle item</li>
  <li>First item</li>
</ol>

The CSS reversed() function gives you the same control programmatically.

text-decoration in ::selection

Browsers now support text-decoration properties inside ::selection, letting you change or remove underlines, overlines, and strikethroughs when text is highlighted by the user.

1
2
3
4
5
::selection {
  background: #4f46e5;
  color: white;
  text-decoration: none; /* remove underlines when selected */
}

Removing underlines during selection prevents them from looking awkward against the selection background color.

1
2
3
4
5
a::selection {
  background: #fbbf24;
  color: black;
  text-decoration-color: black; /* match the selection text color */
}

Only a limited set of properties work in ::selection:

  • color
  • background-color
  • text-decoration and its longhands
  • text-shadow
  • stroke-color, fill-color, stroke-width (SVG)

video-dynamic-range media query

The video-dynamic-range media feature is similar to dynamic-range but specifically targets the video rendering capabilities of the display, which can differ from its general display capabilities.

1
2
3
4
5
6
7
@media (video-dynamic-range: standard) {
  /* Standard dynamic range video */
}

@media (video-dynamic-range: high) {
  /* HDR video capable display */
}

Some devices have separate pipelines for video and graphics rendering. A display may support HDR video (HLG, HDR10) while the graphics compositor uses standard dynamic range — or vice versa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* General display HDR */
@media (dynamic-range: high) { }

/* Video-specific HDR */
@media (video-dynamic-range: high) { }

/* practical use: */
@media (video-dynamic-range: high) {
  video {
    /* Apply HDR-specific styles or hints */
    color-gamut: p3;
  }
}
  • Adapting video player UI for HDR content
  • Showing or hiding HDR badges on video thumbnails
  • Providing HDR-quality poster images for video elements

display: ruby

Ruby annotations are small text displayed above or beside base text, commonly used in Japanese, Chinese, and Korean content to show pronunciation guides (furigana/bopomofo) or glosses.

1
2
3
<ruby>
  漢字 <rt>かんじ</rt>
</ruby>

The <rt> element holds the annotation. Browsers render it above the base text by default.

1
2
3
ruby    { display: ruby; }
rt      { display: ruby-text; }
rp      { display: none; }

These are the default mappings, but you can apply them manually for custom elements or to restore behaviour after a reset.

Accent color

accent-color sets the highlight color used by checkboxes, radio buttons, range sliders, and progress elements — bringing them into your brand with a single line of CSS.

1
2
3
:root {
  accent-color: #7c3aed;
}

Every checkbox, radio, range, and progress element on the page now uses your brand color.

1
2
input[type="checkbox"] { accent-color: #16a34a; }
input[type="range"]    { accent-color: #dc2626; }
  • <input type="checkbox">
  • <input type="radio">
  • <input type="range">
  • <progress>

The browser automatically adjusts the checkmark or thumb color to ensure legible contrast against your chosen accent. You don’t need to worry about white-on-light or black-on-dark issues.

Custom form control colors previously required hiding the native element and building a replacement from scratch with custom HTML and CSS. accent-color achieves a good result in one property.

1
2
3
4
5
6
<style>
    [type=radio],
    [type=checkbox] {
        accent-color: red;
    }
</style>

CSS ::marker

The ::marker pseudo-element targets the marker box of a list item — the bullet, number, or any other counter symbol — letting you style it independently from the list item’s content.

::marker supports a limited set of CSS properties:

  • color
  • content
  • font-* properties
  • unicode-bidi and direction
  • white-space

Layout properties like margin and padding do not apply to ::marker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<style>
li::marker {
    content: "# ";
    font-size: 1.2em;
    font-style: italic;
    font-weight: bold;
}
</style>
<p>Frontend language</p>
<ul>
  <li>Html</li>
  <li>Css</li>
  <li>Javascript</li>
  <li>Svg</li>
</ul>

Frontend language

  • Html
  • Css
  • Javascript
  • Svg

Font metric overrides

Font metric overrides — ascent-override, descent-override, line-gap-override, and size-adjust — let you tweak the metrics of a fallback font so it matches your web font’s dimensions almost exactly.

When a web font loads, it often has different dimensions than the fallback font, causing text to reflow and shift layout — harming your CLS score.

Define a custom @font-face for your fallback that overrides its metrics to match the web font:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@font-face {
  font-family: "FallbackForInter";
  src: local("Arial");
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 107%;
}

body {
  font-family: "Inter", "FallbackForInter", sans-serif;
}

Tools like Font Style Matcher or Capsize can calculate the correct override percentages for any web font and fallback pair.

The page renders with virtually zero layout shift as the font loads, because the fallback takes up the same space as the web font.

Text justify

The text-justify CSS property sets the justification method of text when text-align: justify is set.

UI Fonts

CSS provides generic font family keywords that map directly to the operating system’s interface fonts, letting your web app feel native on every platform.

1
2
3
4
5
6
7
body {
  font-family: ui-sans-serif, system-ui, sans-serif;
}

code {
  font-family: ui-monospace, monospace;
}
KeywordMaps to (examples)
ui-sans-serifSan Francisco (macOS/iOS), Segoe UI (Windows)
ui-serifNew York (macOS)
ui-monospaceSF Mono (macOS), Consolas (Windows)
ui-roundedSF Pro Rounded (iOS)

Why Use System Fonts?

  • Zero load time — no web font download required
  • Native feel — matches the OS the user is on
  • Excellent readability — optimised for screens at all sizes

Before these keywords, there was a long fallback stack:

1
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;