TocTable of Contents

Css baseline 2015

Overview of 2015 baseline css features

Most features are Widely available because they all passed 30 months since being newly available. The keystone date is never before the release date of the youngest browser in the core browser set (presently 2015-07-28, the release date of Edge 12). So that’s why this list starts with 2015.

::first-letter

The ::first-letter pseudo-element targets the first letter (or character) of the first line of a block-level element, allowing it to be styled independently. It is commonly used to create drop caps — an enlarged initial letter at the start of a paragraph — a typographic convention with a long history in print design.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
p::first-letter {
  font-size: 3em;
  font-weight: bold;
  float: left;
  line-height: 0.8;
  margin-right: 0.1em;
}

.article-intro::first-letter {
  font-family: 'Georgia', serif;
  font-size: 4rem;
  color: var(--color-accent);
}

Only a limited set of CSS properties apply to ::first-letter: font properties, text decoration, color, background, margins, padding, borders, float, vertical-align (when not floated), line-height, and word-spacing. The pseudo-element is not available on inline elements.

The browser determines what counts as the “first letter” — punctuation marks immediately preceding the first letter are included in the pseudo-element, and for languages where a letter is composed of multiple characters (like digraphs), the entire letter may be selected.

::first-line

The ::first-line pseudo-element matches the first formatted line of a block-level element. The selection is dynamic — if the container is resized and more or fewer words fit on the first line, the styled range adjusts automatically to always cover exactly the first rendered line.

1
2
3
4
5
6
7
8
9
p::first-line {
  font-variant: small-caps;
  letter-spacing: 0.05em;
}

.article-body::first-line {
  font-weight: bold;
  color: var(--color-primary);
}

Only a subset of CSS properties can be applied to ::first-line: font properties, color, background, word-spacing, letter-spacing, text-decoration, text-transform, line-height, text-shadow, and vertical-align. Layout properties that would affect the shape of the line — like margin or padding — are not permitted.

::first-line applies only to block containers. It has no effect on inline elements. The styled range is determined after all other layout and text-wrapping has been calculated, so the pseudo-element always reflects the actual rendered first line rather than a fixed character count.

:empty

The :empty pseudo-class matches elements that have no children — no child elements, no text nodes, and no whitespace. It allows you to apply styles specifically when an element contains nothing, which is useful for hiding placeholder containers or applying different styles to empty states.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.notification-badge:empty {
  display: none;
}

.card-body:empty {
  padding: 0;
}

td:empty {
  background: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 4px,
    #f0f0f0 4px,
    #f0f0f0 8px
  );
}

An important subtlety: :empty does not match an element that contains only whitespace in most contexts — a single space or newline between opening and closing tags means the element is not considered empty. This can make :empty behave unexpectedly when HTML is formatted with indentation, as the whitespace between tags counts as a text node. HTML-minifying tools or careful template authoring can address this.

Comments inside an element do not prevent it from matching :empty. The pseudo-class is most reliable for elements whose emptiness is controlled programmatically, such as badge counts or dynamically populated containers.

:lang()

The :lang() pseudo-class matches elements based on the language they are in, as determined by the lang attribute or inherited language information. It allows language-specific styles to be applied without class names or additional markup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
:lang(ar) {
  direction: rtl;
  font-family: 'Noto Sans Arabic', sans-serif;
}

:lang(ja) {
  font-family: 'Noto Sans JP', sans-serif;
  line-height: 1.8;
}

:lang(de) q {
  quotes: '„' '"' '‚' ''';
}

:lang(fr) q {
  quotes: '«\00A0' '\00A0»' '‹\00A0' '\00A0›';
}

:lang() matches based on the element’s language tag, including inheritance — an element inside a <div lang="fr"> matches :lang(fr) even without its own lang attribute. This is more reliable than using an attribute selector like [lang="fr"], which only matches elements with the attribute explicitly present.

The selector accepts language range matching, so :lang(zh) matches elements in any Chinese variant (zh-Hans, zh-Hant, zh-TW, etc.). This makes it practical for applying broad typographic rules to a language family without needing to enumerate every subtag.

:lang() is especially useful for multilingual documents where different sections need different quotation marks, fonts, text direction, or spacing.

:nth-child()

The :nth-child() pseudo-class matches elements based on their position among all siblings within a parent, regardless of element type. It uses an An+B formula where A is the step size, B is the starting offset, and n counts from zero upward.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Alternate row colors in a list */
li:nth-child(odd) {
  background: #f9f9f9;
}

li:nth-child(even) {
  background: white;
}

/* Every third item starting from the third */
.grid-item:nth-child(3n) {
  border-right: none;
}

/* First four items only */
.item:nth-child(-n+4) {
  font-weight: bold;
}

The keywords odd and even are shorthand for 2n+1 and 2n. Negative values of A count backwards, so -n+4 matches the first four elements (positions 4, 3, 2, 1 as n goes 0, 1, 2, 3).

Modern browsers support an optional selector argument inside :nth-child(), allowing you to filter by element type while still counting all siblings for the position:

1
2
3
4
/* The second paragraph, counting only p elements */
:nth-child(2 of p) {
  margin-top: 2rem;
}

Related pseudo-classes include :first-child, :last-child, :nth-last-child() (counts from the end), and :only-child (matches when there is exactly one child). Together these give precise positional control over any element in a sibling group.

:nth-of-type() pseudo-classes

The :nth-of-type() pseudo-class matches elements based on their position among siblings of the same element type. Unlike :nth-child(), which counts all sibling elements regardless of type, :nth-of-type() counts only siblings that share the same tag name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Every other paragraph */
p:nth-of-type(odd) {
  background: #f9f9f9;
}

/* Every third image */
img:nth-of-type(3n) {
  border: 2px solid var(--color-accent);
}

/* Second h2 on the page */
h2:nth-of-type(2) {
  margin-top: 3rem;
}

The argument follows the same An+B formula used by :nth-child(): n is an integer counter starting at 0, A is the step, and B is the offset. The keywords odd and even are shorthand for 2n+1 and 2n respectively.

The family of related pseudo-classes includes :first-of-type, :last-of-type, and :nth-last-of-type(). The latter counts from the end rather than the start, so :nth-last-of-type(1) is equivalent to :last-of-type.

:nth-of-type() is particularly useful when a container mixes different element types and you need to target specific elements of one type by position, without being affected by the positions of other element types in between.

:root

The :root pseudo-class matches the root element of the document — in HTML, this is always the <html> element. It is equivalent to the html type selector but has higher specificity, and is the conventional place to declare CSS custom properties (variables) that need to be available throughout the entire document.

1
2
3
4
5
6
7
:root {
  --color-primary: oklch(55% 0.2 250);
  --color-text: #1a1a1a;
  --font-size-base: 1rem;
  --spacing-unit: 0.25rem;
  --border-radius: 4px;
}

Declaring custom properties on :root makes them inherited by all elements, effectively creating global CSS variables. This is the foundation of most CSS design token systems — a single location to define values that are then referenced throughout the stylesheet with var().

:root is also used for global opt-ins to new CSS behaviours. For example:

1
2
3
:root {
  interpolate-size: allow-keywords;
}

This sets the opt-in once at the document level, enabling intrinsic-size animations for all elements on the page. Similarly, color-scheme and view-transition-name are often set on :root to apply document-wide behaviour.

:target

The :target pseudo-class matches an element whose id matches the fragment identifier in the current URL — the part after the #. When a user navigates to page.html#section-2, the element with id="section-2" matches :target.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
:target {
  scroll-margin-top: 5rem; /* Account for sticky header */
}

.section:target {
  background: oklch(97% 0.02 250);
  outline: 2px solid var(--color-primary);
  border-radius: 4px;
}

:target h2 {
  color: var(--color-primary);
}

:target is commonly used to highlight the section a user has navigated to via an anchor link, providing visual feedback that the correct destination has been reached. This is especially useful in long documents with a table of contents.

A creative application is using :target for pure-CSS interactive components — tab panels, modals, and accordions — by linking to different fragment identifiers and styling the targeted element as “active”. While this technique requires no JavaScript, it does modify the browser history and URL on each interaction, which can affect the back button behaviour and accessibility, so it should be used with those trade-offs in mind.

scroll-margin-top on :target is a practical addition that prevents a sticky header from obscuring the targeted element when the browser scrolls to it.

@charset

The @charset at-rule specifies the character encoding of a CSS stylesheet. When present, it must be the very first statement in the file — before any other rules, comments, or whitespace — and it must use double quotes around the encoding name.

1
2
3
4
5
@charset "UTF-8";

body {
  font-family: sans-serif;
}

In practice, @charset "UTF-8" is the only value you will encounter in modern web development. UTF-8 encodes the full Unicode character set and is the standard encoding for all web content.

The rule exists primarily for cases where the stylesheet contains non-ASCII characters directly in the source — for example, string values, content property text, or comments written in languages that use characters outside the basic Latin range. When the server sends a stylesheet with an explicit Content-Type: text/css; charset=UTF-8 HTTP header, @charset is redundant, as the HTTP header takes precedence. Similarly, a UTF-8 byte order mark (BOM) at the start of the file also takes priority.

For most workflows today — where files are saved as UTF-8 and served with correct headers — @charset is not strictly necessary, but including it causes no harm and can prevent encoding issues in edge cases where the HTTP header is absent or incorrect.

@import

The @import at-rule imports an external stylesheet into the current one. It must appear before any other rules in a stylesheet (except @charset and @layer declarations), and causes the browser to fetch and apply the referenced file as if its contents were inserted at that point.

1
2
3
@import url('reset.css');
@import url('typography.css');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600');

@import supports conditional loading via media queries and supports() conditions:

1
2
3
@import url('print.css') print;
@import url('wide.css') screen and (min-width: 1200px);
@import url('grid.css') supports(display: grid);

@import also supports the layer() function, which wraps the imported stylesheet in a named cascade layer — a clean way to integrate third-party styles at a controlled priority level:

1
@import url('third-party.css') layer(vendor);

One performance consideration: @import is blocking and sequential. Each imported file must be fetched before the browser can discover further imports within it, creating a chain that delays rendering. For production use, tools that bundle CSS files together are generally preferable to relying on @import at runtime.

@namespace

The @namespace at-rule declares an XML namespace to be used in CSS selectors. It allows stylesheets to target elements belonging to a specific XML namespace, which is primarily relevant in mixed-namespace documents such as XHTML pages that embed SVG or MathML content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@namespace url('http://www.w3.org/1999/xhtml');
@namespace svg url('http://www.w3.org/2000/svg');

/* Targets only XHTML 'a' elements */
a {
  color: blue;
}

/* Targets only SVG 'a' elements */
svg|a {
  color: inherit;
}

A @namespace declaration with no prefix sets the default namespace — type selectors without a prefix will only match elements in that namespace. A prefixed declaration associates a prefix with a namespace URI, and that prefix can then be used in selectors with the | separator syntax.

@namespace rules must appear after any @charset and @import rules, and before any other at-rules or style rules. In practice, @namespace is rarely needed in standard HTML documents, as HTML parsers handle element namespaces automatically. Its relevance is primarily in SVG stylesheets or XHTML documents where multiple XML namespaces coexist and need to be selectively targeted.

@supports

The @supports at-rule applies a block of CSS rules conditionally, based on whether the browser supports a specified CSS feature. It is the primary tool for progressive enhancement — layering modern CSS on top of a working baseline without breaking browsers that do not yet support the feature.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@supports (display: grid) {
  .layout {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
  }
}

@supports not (display: grid) {
  .layout {
    display: flex;
    flex-wrap: wrap;
  }
}

Conditions can test any property-value pair. The not, and, and or operators allow compound conditions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass {
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }
}

@supports (display: grid) and (gap: 1rem) {
  /* Both must be supported */
}

The selector() function tests whether a selector syntax is understood by the browser:

1
2
3
4
5
@supports selector(:has(> img)) {
  .card:has(> img) {
    padding: 0;
  }
}

The font-tech() and font-format() functions test font technology and format support respectively, useful inside @font-face rules.

@supports queries can also be used in JavaScript via CSS.supports('display', 'grid') or CSS.supports('(display: grid)'), returning a boolean without needing to parse a stylesheet.

<ol>, <ul> and <li>

The <ol>, <ul>, and <li> HTML elements have default browser styles that CSS can override or build on. <ul> renders with bullet markers by default, <ol> with decimal counters, and both have default padding that creates the indented appearance for their list items.

 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
37
38
39
40
41
42
43
44
/* Reset default list styles */
ul, ol {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Custom unordered list */
ul.checklist li {
  padding-left: 1.5rem;
  position: relative;
}

ul.checklist li::before {
  content: '✓';
  position: absolute;
  left: 0;
  color: green;
}

/* Styled ordered list */
ol.steps {
  list-style: none;
  counter-reset: steps;
}

ol.steps li {
  counter-increment: steps;
  padding-left: 3rem;
  position: relative;
}

ol.steps li::before {
  content: counter(steps);
  position: absolute;
  left: 0;
  width: 2rem;
  height: 2rem;
  background: var(--color-primary);
  color: white;
  border-radius: 50%;
  display: grid;
  place-items: center;
}

The ::marker pseudo-element provides a more direct way to style the default list markers — changing their color, size, or content — without replacing them with ::before generated content. list-style-type can accept a string value for a simple custom marker character, or reference a @counter-style rule for more complex marker systems.

Removing list styles with list-style: none also removes the semantic list role in some browsers when combined with display: contents or certain accessibility tree configurations, so ARIA roles may be needed when heavily restyling navigation lists.

2D transforms

CSS 2D transforms move, rotate, scale, and skew elements visually without affecting the document layout. The transformed element occupies its original space in the flow — surrounding elements do not rearrange — making transforms ideal for animation and visual effects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.card:hover {
  transform: translateY(-4px) scale(1.02);
}

.icon {
  transform: rotate(45deg);
}

.skewed-banner {
  transform: skewX(-8deg);
}

.mirrored {
  transform: scaleX(-1);
}

The main 2D transform functions are translate(x, y), translateX(), translateY(), rotate(), scale(x, y), scaleX(), scaleY(), skew(x, y), skewX(), skewY(), and matrix(). Multiple functions can be chained in a single transform declaration — they are applied right to left.

The transform-origin property sets the point around which transformations are applied. It defaults to the element’s centre (50% 50%). Changing it alters how rotations and scales are anchored:

1
2
3
4
.rotating-hand {
  transform-origin: bottom center;
  transform: rotate(30deg);
}

Individual transform properties — translate, rotate, and scale — are now available as standalone properties, making it easier to animate each independently without interfering with the others in the transform shorthand:

1
2
3
4
5
6
.element {
  translate: 0 0;
  rotate: 0deg;
  scale: 1;
  transition: translate 0.3s ease, scale 0.3s ease;
}

Absolute positioning

Absolute positioning removes an element from the normal document flow and places it at a specific position relative to its nearest positioned ancestor — that is, the closest ancestor with a position value other than static. If no such ancestor exists, the element is positioned relative to the initial containing block (essentially the viewport).

1
2
3
4
5
6
7
8
9
.parent {
  position: relative;
}

.child {
  position: absolute;
  top: 1rem;
  right: 1rem;
}

Because an absolutely positioned element is taken out of flow, it does not occupy space in the layout — surrounding elements behave as if it is not there. This makes it well suited for overlays, badges, tooltips, and decorative elements that need to be placed precisely without affecting the layout of other content.

The top, right, bottom, and left properties control the offset from the edges of the containing block. If none are specified, the element stays in its natural position in the text flow but is still removed from the layout’s influence on other elements.

An absolutely positioned element’s size defaults to shrink-to-fit (wrapping its content), but can be stretched to fill its containing block by setting opposing properties simultaneously, such as left: 0 and right: 0.

Animations (CSS)

CSS animations allow elements to transition through a sequence of styles over time, defined by @keyframes rules and controlled by a set of animation-* properties. Unlike transitions, animations can run automatically without a state change trigger, loop indefinitely, and move through multiple intermediate steps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.alert {
  animation: fade-in 0.3s ease forwards;
}

The animation shorthand combines animation-name, animation-duration, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode, and animation-play-state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.spinner {
  animation: rotate 1s linear infinite;
}

@keyframes rotate {
  to { transform: rotate(360deg); }
}

.pulse {
  animation: pulse 2s ease-in-out 0.5s 3 alternate both;
}

animation-fill-mode: forwards keeps the final keyframe state applied after the animation ends. backwards applies the first keyframe during the delay period. both does both. animation-direction: alternate reverses the animation on each odd iteration, useful for back-and-forth effects.

Multiple animations can be applied to a single element as a comma-separated list. Respecting prefers-reduced-motion by wrapping decorative animations in a media query is an important accessibility consideration.

background

The background shorthand sets all background-related properties in a single declaration: background-color, background-image, background-repeat, background-position, background-size, background-origin, background-clip, and background-attachment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.hero {
  background: url('image.jpg') center / cover no-repeat #1a1a1a;
}

.striped {
  background: repeating-linear-gradient(
    45deg,
    #f0f0f0,
    #f0f0f0 10px,
    white 10px,
    white 20px
  );
}

When background-size is included, it must immediately follow background-position separated by a slash: center / cover. background-color can only be set on the last (or only) layer and always sits beneath all image layers.

Multiple background layers can be stacked using a comma-separated list. Each layer corresponds to a separate background-image, with individual position, size, repeat, origin, and clip settings. The first layer in the list is rendered on top.

1
2
3
4
5
6
.layered {
  background:
    url('overlay.png') top right no-repeat,
    url('texture.png') repeat,
    #f5f0eb;
}

As with other shorthand properties, any longhand not explicitly set in the background shorthand is reset to its initial value, which can unintentionally override earlier declarations. Using the individual longhand properties provides more predictable cascade behaviour when building on existing styles.

background-attachment

The background-attachment property controls whether a background image scrolls with the page or remains fixed relative to the viewport. It accepts three values: scroll, fixed, and local.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.hero {
  background-image: url('landscape.jpg');
  background-attachment: fixed; /* Parallax effect */
  background-size: cover;
  background-position: center;
}

.card {
  background-attachment: scroll; /* Default: moves with element */
}

.scrollable-box {
  overflow: auto;
  background-image: url('pattern.png');
  background-attachment: local; /* Scrolls with the element's content */
}

scroll (the default) fixes the background relative to the element itself — the background moves with the element as the page scrolls, but does not scroll with the element’s own overflow content. fixed fixes the background relative to the viewport, creating a parallax-like effect where the background stays stationary as the page scrolls. local fixes the background relative to the element’s content, so it scrolls along when the element’s own overflow is scrolled.

background-attachment: fixed creates a stacking context and can interact unexpectedly with background-size: cover on mobile devices, where fixed backgrounds are often treated as scroll for performance reasons. For reliable cross-device parallax effects, CSS transform-based or scroll-driven animation approaches are generally more predictable.

background-clip

The background-clip property controls how far the background — color and image — extends within an element’s box. It clips the background to a specific area: the border box, padding box, content box, or just the text.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.border-box {
  background-clip: border-box; /* Default: extends under the border */
}

.padding-box {
  background-clip: padding-box; /* Stops at the inner edge of the border */
}

.content-box {
  background-clip: content-box; /* Confined to the content area only */
}

.gradient-text {
  background: linear-gradient(135deg, #f093fb, #f5576c);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

The text value clips the background to the foreground text, allowing gradients and images to show through letterforms. Setting color: transparent makes the text itself invisible so only the clipped background is visible, producing the gradient text effect. Both the prefixed -webkit-background-clip: text and the unprefixed form are typically included for compatibility.

The newer border-area value clips the background to the border region only — excluding the padding and content — making it straightforward to paint gradients directly onto borders without pseudo-elements or border-image.

When multiple background layers are used, background-clip accepts comma-separated values to configure each layer independently.

background-color

The background-color property sets the background color of an element, filling its content area, padding, and (by default) the area behind its border. It accepts any valid CSS color value, including named colors, hex values, rgb(), hsl(), oklch(), and the transparent keyword.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.card {
  background-color: #f5f5f5;
}

.highlight {
  background-color: rgb(255 235 59 / 0.4);
}

.glass {
  background-color: transparent;
}

background-color is rendered beneath any background-image set on the same element, so it acts as a fallback when an image fails to load, and as the base layer visible through transparent areas of the image.

Unlike background-image, background-color cannot be layered — only one background color applies per element. However, it interacts with background-clip, which controls how far the color extends: border-box (default), padding-box, or content-box. Setting background-color to transparent is equivalent to its initial value and is a common way to explicitly remove an inherited or default background.

background-image

The background-image property sets one or more images as the background of an element. It accepts url() references to image files, CSS gradient functions, or none. Multiple images can be layered by providing a comma-separated list, with the first image rendering on top.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.card {
  background-image: url('pattern.svg');
}

.gradient-header {
  background-image: linear-gradient(135deg, #667eea, #764ba2);
}

.layered {
  background-image:
    url('icon.png'),
    radial-gradient(circle at top right, #f093fb, transparent);
}

background-image is always rendered on top of background-color, which acts as a fallback when the image fails to load and shows through transparent areas of the image.

The full range of CSS gradient types — linear-gradient(), radial-gradient(), conic-gradient(), and their repeating-* variants — are valid image values. This makes it possible to create geometric patterns, color transitions, and decorative effects without any image files.

background-image works in conjunction with background-repeat (controlling tiling), background-position (controlling placement), background-size (controlling dimensions), and background-clip (controlling which area the image covers). Each of these properties also accepts comma-separated values to configure each image layer independently.

background-origin

The background-origin property determines the reference area used for positioning and sizing a background image. It defines the origin point from which background-position is calculated and the area that percentage values in background-size are relative to.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.border-box-origin {
  background-origin: border-box; /* Position relative to border edge */
}

.padding-box-origin {
  background-origin: padding-box; /* Default: relative to padding edge */
}

.content-box-origin {
  background-origin: content-box; /* Position relative to content edge */
}

The default value is padding-box, which means background-position: 0 0 places the image at the top-left corner of the padding area — inside the border. With border-box, 0 0 positions the image at the very top-left corner of the border itself. With content-box, 0 0 aligns to the top-left of the content area, excluding padding.

background-origin is ignored when background-attachment is set to fixed, as fixed backgrounds are positioned relative to the viewport regardless.

The difference between background-origin and background-clip is subtle but important: background-origin determines where the image is positioned and sized from, while background-clip determines where the background is visible up to. They are independent — you can position a background relative to the border box but clip it to the padding box, for example.

When using multiple background layers, background-origin accepts comma-separated values corresponding to each layer.

background-position

The background-position property sets the starting position of a background image within its background positioning area (determined by background-origin). It accepts keywords, lengths, and percentages, and can be defined as a single value or a pair of values for horizontal and vertical axes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.hero {
  background-image: url('photo.jpg');
  background-position: center; /* Centre on both axes */
}

.top-right {
  background-position: top right;
}

.offset {
  background-position: 20px 40px; /* 20px from left, 40px from top */
}

.percentage {
  background-position: 75% 25%;
}

When a single keyword or value is given, the other axis defaults to center. The keyword pairs left, center, right (horizontal) and top, center, bottom (vertical) can be combined in any order when both are named keywords.

A four-value syntax allows offset from any edge:

1
2
3
4
.edge-offset {
  /* 20px from the right edge, 30px from the bottom */
  background-position: right 20px bottom 30px;
}

Percentage values are relative to the difference between the background positioning area and the background image size — 50% on both axes is equivalent to center center, meaning the image’s centre aligns with the area’s centre.

When multiple background layers are used, background-position accepts comma-separated values, one per layer.

background-size

The background-size property sets the dimensions of a background image. It accepts length values, percentages, and the keywords auto, cover, and contain.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.hero {
  background-image: url('photo.jpg');
  background-size: cover; /* Fill the container, cropping if necessary */
  background-position: center;
}

.logo {
  background-image: url('logo.svg');
  background-size: contain; /* Fit entirely within the container */
  background-repeat: no-repeat;
}

.pattern {
  background-image: url('tile.png');
  background-size: 40px 40px; /* Explicit dimensions */
}

.wide {
  background-size: 100% auto; /* Full width, auto height */
}

cover scales the image to be as large as possible while ensuring it covers the entire background area — some parts of the image may be cropped if the aspect ratio differs from the container. contain scales the image to fit entirely within the background area without cropping, which may leave uncovered space (filled by background-color or tiling if background-repeat allows it).

Two values set width and height independently. A single value sets the width, with height defaulting to auto to preserve the image’s aspect ratio. Percentage values are relative to the background positioning area (determined by background-origin).

When using multiple background layers, background-size accepts a comma-separated list of values, with each value corresponding to a layer in background-image.

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.

border-radius

The border-radius shorthand property rounds the corners of an element’s outer border edge. It accepts one to four values following the standard shorthand pattern for top-left, top-right, bottom-right, and bottom-left corners. A single value rounds all four corners equally.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.card {
  border-radius: 8px;
}

.pill {
  border-radius: 999px; /* Large value produces a fully rounded pill shape */
}

.asymmetric {
  border-radius: 4px 16px; /* top-left/bottom-right, top-right/bottom-left */
}

.circle {
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
}

Each corner can independently have an elliptical radius by providing two values separated by a slash — the first for the horizontal radius and the second for the vertical:

1
2
3
4
5
6
7
.elliptical {
  border-radius: 50% / 30%; /* Horizontal / vertical radii */
}

.leaf {
  border-radius: 0 100% 0 100%; /* Alternating corners for organic shapes */
}

border-radius applies to the element’s background, border, and box-shadow — but not to child elements or content, which can overflow the rounded corners unless overflow: hidden is set on the element.

The longhand properties — border-top-left-radius, border-top-right-radius, border-bottom-right-radius, and border-bottom-left-radius — each accept the two-value horizontal/vertical syntax individually for precise control over each corner.

Borders

The border shorthand property sets the width, style, and color of an element’s border in a single declaration. Each of these components can also be set individually with border-width, border-style, and border-color, or per-side with properties like border-top, border-right, border-bottom, and border-left.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.box {
  border: 2px solid #333;
}

.subtle {
  border-bottom: 1px dashed #ccc;
}

.rounded {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

border-style accepts values including solid, dashed, dotted, double, groove, ridge, inset, and outset. A border must have a non-none style to be visible, regardless of width and color.

border-radius controls the rounding of corners and works independently of the border itself — you can have rounded corners on an element with no border, and the rounding will apply to the background and any outline. For more decorative borders — gradients, images, or complex shapes — border-image provides an alternative to a solid colored border.

box-shadow

The box-shadow property adds one or more shadows to an element’s box. Each shadow is defined by a horizontal offset, a vertical offset, an optional blur radius, an optional spread radius, and a color. Multiple shadows are stacked in a comma-separated list, with the first shadow appearing on top.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.card {
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}

.elevated {
  box-shadow:
    0 1px 2px rgb(0 0 0 / 0.08),
    0 4px 16px rgb(0 0 0 / 0.12);
}

.inset-shadow {
  box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.15);
}

.glow {
  box-shadow: 0 0 16px 4px oklch(70% 0.3 200 / 0.6);
}

The horizontal and vertical offsets move the shadow from the element’s position — positive values go right and down. The blur radius softens the edge; a value of 0 produces a hard-edged shadow. The spread radius expands or contracts the shadow beyond the element’s dimensions — positive values grow it, negative values shrink it.

The inset keyword moves the shadow inside the element’s border, creating a recessed or inner-glow effect.

Layering multiple shadows with slightly different offsets, blurs, and opacities produces more natural-looking elevation effects than a single shadow. box-shadow respects border-radius — rounded corners produce rounded shadows. Unlike filter: drop-shadow(), box-shadow always follows the rectangular box of the element and does not follow the shape of transparent PNG images or SVG content.

box-sizing

The box-sizing property controls how an element’s total width and height are calculated — specifically whether padding and border are included within the declared dimensions or added outside them.

The default value, content-box, sizes the element to its content area only. Padding and border are added on top of the declared width and height, making the rendered element larger than its specified dimensions:

1
2
3
4
5
6
7
.content-box {
  box-sizing: content-box; /* Default */
  width: 300px;
  padding: 20px;
  border: 2px solid;
  /* Rendered width: 300 + 40 + 4 = 344px */
}

border-box includes padding and border within the declared dimensions, so the element renders at exactly the specified width and height:

1
2
3
4
5
6
7
.border-box {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 2px solid;
  /* Rendered width: exactly 300px */
}

border-box is almost universally considered more intuitive and is applied globally in most modern projects via a reset:

1
2
3
*, *::before, *::after {
  box-sizing: border-box;
}

This reset makes sizing predictable throughout a codebase — a width: 100% element with padding will not overflow its container. It is included in virtually all CSS resets and design system base stylesheets.

calc()

The calc() function performs mathematical calculations within CSS property values, allowing different unit types to be mixed in a single expression. It accepts addition (+), subtraction (-), multiplication (*), and division (/).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.sidebar {
  width: calc(100% - 2rem);
}

.element {
  padding: calc(1rem + 4px);
  font-size: calc(1rem * 1.25);
}

.centered {
  left: calc(50% - 150px); /* Centre a 300px element */
}

The most powerful aspect of calc() is mixing units — 100% - 2rem is not possible with any single unit, but calc() resolves both sides to their concrete pixel values at render time and computes the result. Whitespace around + and - operators is required; * and / do not require it but it is recommended for readability.

calc() can reference CSS custom properties:

1
2
3
4
5
6
7
:root {
  --nav-height: 60px;
}

.content {
  height: calc(100vh - var(--nav-height));
}

Nesting calc() inside itself is valid but redundant — the inner calc() can be omitted. More specialised math functions build on the same foundation: min(), max(), and clamp() for range constraints; round(), mod(), rem(), sin(), cos(), tan(), pow(), sqrt(), log(), and exp() for advanced calculations. calc-size() extends the concept to intrinsic size keywords like auto and fit-content.

ch unit

The ch unit represents the width of the “0” (zero) character in the current font. It provides a character-width-relative unit, making it particularly useful for sizing text inputs, code blocks, and containers where a meaningful width expressed in terms of characters is more intuitive than an abstract length.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
input[type="text"] {
  width: 30ch; /* Room for approximately 30 characters */
}

.code-block {
  max-width: 80ch; /* Classic terminal line length */
}

.narrow-column {
  width: 45ch;
}

Because ch is based on the “0” character — which in proportional fonts is typically close to the average character width — a width of 60ch gives a rough approximation of 60 characters of text, though actual character count varies with the content. In monospace fonts, “0” has the same width as every other character, so ch is perfectly accurate for character counting in code contexts.

ch responds to changes in font-size and font-family, scaling appropriately as the font changes. Its root-relative counterpart, rch, always references the root element’s font rather than the current element’s font.

Color

The color property sets the foreground color of an element’s text content and text decorations, and establishes the value of the currentcolor keyword for that element and its descendants. It accepts any valid CSS color value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
body {
  color: #1a1a1a;
}

a {
  color: oklch(50% 0.2 250);
}

.muted {
  color: rgb(0 0 0 / 0.5);
}

color is inherited, so setting it on a parent element cascades down to all children unless overridden. This makes it efficient for establishing a base text color on body or a component root.

The currentcolor keyword is one of color’s most useful side effects — it exposes the computed text color as a reusable value for other properties. Setting border-color: currentcolor or fill: currentcolor on an SVG icon, for example, means those values automatically track the element’s text color without needing to repeat it.

CSS now supports a wide range of color spaces beyond the traditional sRGB model, including oklch, display-p3, and others, accessible through modern color functions for more precise and perceptually uniform color choices.

Content

The content property specifies the content to be rendered by a ::before or ::after pseudo-element. It is what makes generated content possible — inserting text, images, counters, or attribute values into the document without modifying the HTML.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.required::after {
  content: ' *';
  color: red;
}

a[href^="https"]::before {
  content: url('/icons/external.svg');
}

.chapter::before {
  content: 'Chapter ' counter(chapter) ': ';
  counter-increment: chapter;
}

content accepts a string, a url() pointing to an image, counter() and counters() for CSS counters, attr() to pull in an HTML attribute value, and special keywords like open-quote and close-quote. Multiple values can be concatenated by listing them with spaces.

Setting content: none or content: normal on a pseudo-element suppresses it entirely, which is useful for resetting generated content in certain contexts.

The property also supports an accessibility alt text via the slash syntax — content: url('/icon.svg') / 'Home' — allowing generated images or symbols to carry a meaningful text alternative for assistive technologies.

Counters (CSS)

CSS counters allow you to maintain and display automatically incrementing numbers in generated content, without JavaScript. They are defined and manipulated using counter-reset, counter-increment, and counter-set, and displayed using the counter() and counters() functions inside the content property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
body {
  counter-reset: section;
}

h2 {
  counter-increment: section;
}

h2::before {
  content: counter(section) '. ';
}

counter-reset initialises a counter to a given value (default 0) on an element. counter-increment adds to the counter each time the element appears. counter() retrieves the current value for display.

Counters can be nested using counters(), which outputs all levels of a counter separated by a specified string — useful for hierarchical numbering like “1.2.3” in outlines and legal documents:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ol {
  counter-reset: item;
}

li {
  counter-increment: item;
}

li::marker {
  content: counters(item, '.') ' ';
}

The display style of a counter can be changed by passing a list style type as a second argument to counter(): counter(section, upper-roman) outputs Roman numerals. CSS counters work entirely in the browser without scripting and are most commonly used for numbered headings, figures, footnotes, and custom ordered list styles.

cubic-bezier() easing

The cubic-bezier() function defines a custom easing curve for CSS transitions and animations. It takes four numbers representing the x and y coordinates of two control points on a cubic Bézier curve, giving precise control over how a value accelerates and decelerates over the duration of an animation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.smooth {
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* Material ease */
}

.bounce-out {
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.sharp-in {
  transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
}

The four values are x1, y1, x2, y2, where (x1, y1) and (x2, y2) are the two control points. The x values must be between 0 and 1 (they represent time); the y values can exceed this range to produce overshoot and bounce effects.

The built-in keyword easing functions are shorthands for common cubic-bezier values: ease is cubic-bezier(0.25, 0.1, 0.25, 1), ease-in is cubic-bezier(0.42, 0, 1, 1), ease-out is cubic-bezier(0, 0, 0.58, 1), and ease-in-out is cubic-bezier(0.42, 0, 0.58, 1). linear is cubic-bezier(0, 0, 1, 1).

Browser DevTools in Chrome, Firefox, and Safari all include a visual cubic-bezier editor that lets you drag the control points and preview the curve in real time, making it straightforward to dial in the exact feel you want without manual calculation.

currentColor

currentColor is a CSS keyword that resolves to the computed value of the color property on the same element. It allows any property that accepts a color value to automatically inherit and reuse the element’s text color, without repeating the value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.icon {
  color: var(--brand-color);
  border: 2px solid currentColor; /* Same as --brand-color */
  box-shadow: 0 0 8px currentColor;
}

svg {
  fill: currentColor; /* SVG inherits the text color of its parent */
}

.badge {
  color: #c0392b;
  background: transparent;
  border: 1.5px solid currentColor; /* Matches the red text */
}

currentColor is particularly powerful with SVG icons inlined in HTML or used as background images via mask. Setting fill: currentColor on an SVG means the icon automatically adopts the text color of whatever element it is placed inside, making it trivial to create icons that respond to theme changes, hover states, and color overrides without any additional CSS.

Because color is an inherited property, currentColor also inherits — a child element using currentColor will pick up the color of its nearest ancestor that has one set, following the normal inheritance chain.

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.

Display

The display property defines how an element participates in its parent’s layout (its outer display type) and how it lays out its own children (its inner display type). It is one of the most fundamental properties in CSS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.inline-element {
  display: inline;
}

.block-element {
  display: block;
}

.flex-container {
  display: flex;
}

.grid-container {
  display: grid;
}

.hidden {
  display: none;
}

The modern two-value syntax makes the dual nature of display explicit: display: block flex means the element is a block-level box on the outside (in normal flow) but establishes a flex formatting context for its children. Single keywords like flex and grid are shorthands for block flex and block grid respectively.

display: none removes an element from layout entirely — it takes up no space and is invisible to assistive technologies. display: contents is a special value that causes the element itself to generate no box, but its children are treated as if they were direct children of the element’s parent — useful for semantic wrapper elements that should not affect layout.

Changing display is now animatable using transition-behavior: allow-discrete, enabling enter and exit transitions on elements that toggle between display: none and a visible state.

display: list-item

display: list-item makes any element behave like a list item — generating a principal block box for content and a separate marker box for the list bullet or number. This is the default display value for <li> elements, but it can be applied to any element to give it list-item behaviour without using a list element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.custom-list-item {
  display: list-item;
  list-style-type: disc;
  margin-left: 1.5rem;
}

dt {
  display: list-item;
  list-style-type: '→ ';
}

Once display: list-item is set, the full range of list-style properties apply: list-style-type for the marker character or counter style, list-style-position for whether the marker sits inside or outside the content box, and list-style-image for a custom image marker.

This is particularly useful when semantic list markup is not appropriate but a visual list treatment is desired, or when existing non-list elements need to be presented in a list-like format without restructuring the HTML. The marker box generated by display: list-item is also targetable via the ::marker pseudo-element for custom styling.

em unit

The em unit is a relative length unit equal to the computed font-size of the element it is used on. If an element has a font-size of 16px, then 1em equals 16px for any property on that element — including margin, padding, width, and border-radius.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.component {
  font-size: 1.125rem;
  padding: 0.75em 1em; /* Relative to 1.125rem, not the root */
  border-radius: 0.25em;
}

h2 {
  font-size: 1.5rem;
  margin-bottom: 0.5em; /* Half of h2's own font-size */
}

When em is used on the font-size property itself, it is relative to the parent element’s font-size, not the element’s own. This is the source of the compounding effect em is known for — nested elements can unintentionally multiply their font sizes if em is used throughout.

This compounding behaviour is avoided by rem, which always references the root font size. em remains the preferred unit when you want spacing and sizing to scale proportionally with the local font size of a component — useful in design systems where components may be embedded at different sizes.

ex unit

The ex unit represents the x-height of the current font — approximately the height of a lowercase “x”. Unlike em, which is based on the full em square, ex is tied to the visual height of lowercase letters, making it useful for fine typographic adjustments that need to feel optically proportional to the text rather than mathematically proportional to the font size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sup {
  /* Raise superscript relative to the x-height of the surrounding text */
  vertical-align: 1ex;
  font-size: 0.75em;
}

.icon {
  /* Size icon to match lowercase letter height */
  height: 1ex;
  width: 1ex;
}

The x-height varies significantly between typefaces — a font with tall lowercase letters will have a larger ex value than one with short lowercase letters, even at the same font-size. This makes ex more visually consistent across fonts than em for adjustments that need to align with the apparent height of lowercase text.

In practice, ex is most commonly used for vertical-align adjustments on inline elements like superscripts, subscripts, and inline icons, where alignment relative to the lowercase letter body produces better optical results than alignment relative to the baseline or cap height alone.

Fixed positioning

Fixed positioning places an element relative to the viewport and keeps it there as the page scrolls. Like absolute positioning, it removes the element from the normal document flow, but its containing block is always the viewport rather than a positioned ancestor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.sticky-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
}

.back-to-top {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
}

Fixed elements are commonly used for navigation bars, cookie banners, floating action buttons, and chat widgets — anything that should remain visible regardless of scroll position.

One important exception to viewport-relative behaviour is when a fixed element’s ancestor has a transform, perspective, filter, or will-change property applied. In those cases, that ancestor becomes the containing block instead of the viewport, which can break the expected fixed behaviour. This is a frequent source of layout bugs when fixed headers or overlays are used alongside animated or transformed parent elements.

Flexbox

Flexbox is a one-dimensional layout model that distributes space and aligns items along a single axis — either a row or a column. A flex container is created by setting display: flex or display: inline-flex on an element, making all direct children flex items.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.nav {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.card-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.sidebar-layout {
  display: flex;
  gap: 2rem;
}

.sidebar {
  flex: 0 0 240px; /* Don't grow, don't shrink, fixed at 240px */
}

.main {
  flex: 1; /* Grow to fill remaining space */
}

The primary axis is set by flex-direction: row (default, left to right), row-reverse, column, or column-reverse. Items align along the cross axis using align-items (stretch by default) and along the main axis using justify-content.

flex-wrap: wrap allows items to wrap onto multiple lines when they exceed the container width. gap sets spacing between items without affecting outer edges.

Each flex item’s behaviour is controlled by three properties: flex-grow (how much the item grows relative to others), flex-shrink (how much it shrinks), and flex-basis (its initial size before growth or shrinkage). These combine into the flex shorthand: flex: 1 is equivalent to flex: 1 1 0, and flex: none to flex: 0 0 auto.

Individual items can override alignment with align-self, and their order can be changed with order without modifying the DOM — though this should be used with care as it separates visual and source order, which affects keyboard navigation and accessibility.

float and clear

The float property shifts an element to the left or right side of its container, allowing inline content to wrap around it. A floated element is taken partially out of normal flow — it still participates in the block formatting context of its parent, but following inline content flows around it rather than being displaced by it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.figure {
  float: right;
  margin: 0 0 1rem 1.5rem;
  max-width: 40%;
}

.pull-quote {
  float: left;
  width: 35%;
  margin: 0 1.5rem 1rem 0;
}

The clear property on a subsequent element prevents it from sitting beside a float. Setting clear: left, clear: right, or clear: both pushes the element below any preceding floats in the specified direction.

1
2
3
.article-footer {
  clear: both;
}

Floats were historically used for multi-column page layouts before flexbox and grid were available. Today their primary use case is the original one they were designed for: wrapping text around images and pull quotes in editorial content. For page layout, flexbox and grid are almost always more appropriate. The display: flow-root value on a container creates a new block formatting context that automatically contains its floated children, replacing the older clearfix hack.

Font shorthand

The font shorthand sets font-style, font-variant, font-weight, font-stretch (now font-width), font-size, line-height, and font-family in a single declaration. The order of values follows a strict syntax, and some components are optional.

1
2
3
4
5
6
7
p {
  font: italic small-caps bold 1rem/1.6 'Georgia', serif;
}

.label {
  font: 600 0.875rem/1 'Inter', sans-serif;
}

The required components are font-size and font-family, which must always appear in that order, at the end of the declaration. line-height follows font-size separated by a slash. The optional components — font-style, font-variant, font-weight, and font-stretch — appear before font-size in any order.

One important caveat: any font-related longhand properties not explicitly set in the shorthand are reset to their initial values. This means using the font shorthand later in a stylesheet can silently undo earlier declarations of individual font properties.

The font shorthand also accepts system font keywords — caption, icon, menu, message-box, small-caption, and status-bar — which adopt the font used by the corresponding OS UI element, allowing web content to match the native system typography.

font-family

The font-family property specifies a prioritised list of font family names and generic family keywords. The browser works through the list from left to right, using the first font that is available on the system or loaded via @font-face.

1
2
3
4
5
6
7
body {
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
}

code {
  font-family: 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
}

Font family names containing spaces must be quoted. Generic family keywords — serif, sans-serif, monospace, cursive, fantasy, and system-ui — do not need quotes and should always be included at the end of the list as an absolute fallback.

Custom fonts are loaded using @font-face, which maps a family name to a font file. The font-display descriptor in @font-face controls how the browser handles the period before the font loads — whether it shows invisible text, a fallback font, or attempts to minimise layout shift.

font-family is an inherited property, so setting it on body or a component root cascades to all descendants unless overridden. Modern best practice often sets font-family using CSS custom properties or a design token system, making it straightforward to change typefaces globally or per-theme.

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-size

The font-size property sets the size of the font for an element’s text. It accepts absolute lengths, relative lengths, percentages, and a set of keyword values. It is one of CSS’s most foundational properties and directly affects the computed values of em, ex, cap, ch, and other font-relative units on the same element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
body {
  font-size: 1rem; /* 16px in most browsers by default */
}

h1 {
  font-size: 2.5rem;
}

.small-print {
  font-size: 0.75em; /* 75% of parent's font-size */
}

.fluid {
  font-size: clamp(1rem, 2.5vw, 1.5rem);
}

rem (root em) is commonly preferred over em for font-size to avoid the compounding effect of nested em values. Percentage values behave identically to emfont-size: 150% means the same as font-size: 1.5em.

Keyword values include absolute sizes (xx-small through xx-large) and relative sizes (smaller, larger) that step up or down relative to the parent. Using clamp() for font-size is a common approach to fluid typography, allowing the size to scale with the viewport while staying within a defined minimum and maximum range.

font-style

The font-style property selects between the normal, italic, and oblique variants of a font. italic requests a true italic face if one is available in the font family — a separately designed variant with letterforms specifically drawn for slanted text. oblique requests a mechanically slanted version of the regular face, used when no italic variant exists.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
em {
  font-style: italic;
}

.annotation {
  font-style: oblique;
}

.upright {
  font-style: normal;
}

For variable fonts that include an ital or slnt axis, font-style: oblique can accept an angle value to fine-tune the degree of slant:

1
2
3
.custom-slant {
  font-style: oblique 12deg;
}

The range of valid angles is -90deg to 90deg. If the font does not support a slant axis, the browser synthesises the slant by skewing the regular face, which is generally considered lower quality than a true italic or oblique face.

font-style is inherited, and is most commonly set on emphasis elements like <em> and <cite>, or used in editorial contexts to distinguish text types such as captions, asides, or foreign-language phrases.

font-variant

The font-variant shorthand controls a range of typographic glyph substitutions and variations offered by OpenType fonts. In its simplest form it toggles small caps, but it is also a shorthand for a family of longhands — font-variant-caps, font-variant-numeric, font-variant-ligatures, font-variant-alternates, font-variant-east-asian, and font-variant-emoji — each targeting different categories of typographic feature.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.small-caps {
  font-variant: small-caps;
}

.numeric {
  font-variant-numeric: oldstyle-nums proportional-nums;
}

.ligatures {
  font-variant-ligatures: common-ligatures discretionary-ligatures;
}

Small caps (font-variant-caps: small-caps) replaces lowercase letters with smaller versions of the capital letterforms — either from a dedicated small caps face or synthesised by the browser. Numeric variants control how digits are rendered: lining versus oldstyle, tabular versus proportional.

Ligature control via font-variant-ligatures enables or disables the automatic substitution of letter pairs (like “fi” or “fl”) with combined glyphs. Common ligatures are typically enabled by default in browsers; discretionary and historical ligatures are off by default and can be opted into for more typographically rich display settings.

The effectiveness of all font-variant features depends on the font supporting the relevant OpenType features.

font-weight

The font-weight property sets the weight (thickness) of the font. It accepts numeric values from 1 to 1000, or keyword equivalents. The most commonly used values are 400 (normal) and 700 (bold), which correspond to the normal and bold keywords.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
body {
  font-weight: 400;
}

h1 {
  font-weight: 700;
}

.light-label {
  font-weight: 300;
}

.black-headline {
  font-weight: 900;
}

Named keywords include thin (100), extra-light (200), light (300), normal (400), medium (500), semi-bold (600), bold (700), extra-bold (800), and black (900). The keywords bolder and lighter are relative, stepping up or down from the inherited weight.

For variable fonts with a weight axis (wght), any value within the font’s supported range can be used — not just multiples of 100. This allows fine-grained weight control for display typography or responsive weight adjustments.

If the requested weight is not available as a distinct face, the browser selects the nearest available weight following a defined fallback algorithm. If no heavier face is available, the browser may synthesise bold by artificially thickening the regular face, though this is generally lower quality than a true bold variant.

Form validity pseudo-classes

CSS provides a set of pseudo-classes that reflect the validity state of form controls, allowing visual feedback to be applied based on whether a field’s current value satisfies its validation constraints — without JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
input:valid {
  border-color: #2ecc71;
}

input:invalid {
  border-color: #e74c3c;
}

input:required {
  border-left: 3px solid var(--color-accent);
}

input:optional {
  border-left: 3px solid #ccc;
}

input:in-range {
  background: #f0fff4;
}

input:out-of-range {
  background: #fff0f0;
}

:valid matches when the field’s value satisfies all its constraints (required, minlength, maxlength, pattern, min, max, type, etc.). :invalid matches when it does not. :required and :optional reflect the presence or absence of the required attribute. :in-range and :out-of-range apply to <input type="number"> and <input type="range"> elements with min and max attributes.

A significant usability concern with :valid and :invalid is that they apply immediately on page load, before the user has interacted with the field at all — an empty required input is :invalid from the start, which can feel jarring. The newer :user-valid and :user-invalid pseudo-classes address this by only applying after the user has interacted with and then left the field, producing a much more appropriate timing for validation feedback.

1
2
3
4
5
6
7
input:user-invalid {
  border-color: #e74c3c;
}

input:user-valid {
  border-color: #2ecc71;
}

getComputedStyle()

getComputedStyle() is a browser JavaScript API — available as window.getComputedStyle() — that returns the final, resolved values of all CSS properties applied to an element after the cascade, inheritance, and any browser defaults have been applied. It reflects what the browser has actually computed and is rendering, not what is written in any individual stylesheet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const element = document.querySelector('.card');
const styles = window.getComputedStyle(element);

const fontSize = styles.getPropertyValue('font-size');
const color = styles.getPropertyValue('color');
const customProp = styles.getPropertyValue('--brand-color');

console.log(fontSize);  // e.g. "18px"
console.log(color);     // e.g. "rgb(26, 26, 26)"
console.log(customProp); // e.g. " oklch(55% 0.2 250)"

The returned CSSStyleDeclaration object can be queried with getPropertyValue() for any CSS property, including custom properties. Values are always returned as strings in their resolved form — lengths in pixels, colors in rgb() notation, and so on.

An optional second argument allows pseudo-element styles to be retrieved:

1
2
const before = window.getComputedStyle(element, '::before');
const content = before.getPropertyValue('content');

getComputedStyle() is read-only — it cannot be used to set styles. It is most commonly used when JavaScript needs to respond to or measure CSS-driven state, such as reading a custom property value set by a media query, measuring an element’s dimensions after layout, or checking whether a transition or animation is currently active.

Gradients

CSS gradients are image values generated by the browser from color stops, usable anywhere an image is accepted — most commonly in background-image. There are three main gradient types: linear, radial, and conic, each with a repeating variant.

linear-gradient() transitions colors along a straight line at a specified angle or direction:

1
2
3
4
5
6
7
.banner {
  background-image: linear-gradient(135deg, #667eea, #764ba2);
}

.subtle {
  background-image: linear-gradient(to bottom, white, #f0f0f0);
}

radial-gradient() radiates colors outward from a central point in a circular or elliptical shape:

1
2
3
.spotlight {
  background-image: radial-gradient(circle at 30% 40%, #fff9c4, transparent 60%);
}

conic-gradient() sweeps colors around a central point, like the face of a pie chart:

1
2
3
4
.pie {
  background-image: conic-gradient(#f093fb 0% 25%, #f5576c 25% 60%, #4facfe 60%);
  border-radius: 50%;
}

The repeating-* variants — repeating-linear-gradient(), repeating-radial-gradient(), and repeating-conic-gradient() — tile the gradient pattern seamlessly, enabling geometric stripe and pattern effects without image files.

color stops can include explicit position values and hints (midpoint control). Modern CSS gradient syntax supports explicit color space interpolation — linear-gradient(in oklch, blue, yellow) — which produces more perceptually uniform color transitions than the default sRGB interpolation.

inherit

The inherit keyword forces a property to take the computed value of the same property from its parent element. It works for both inherited and non-inherited properties — for non-inherited properties, it explicitly opts the element into the inheritance chain that would otherwise not apply.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.child {
  /* border does not inherit by default — this forces it to */
  border: inherit;
}

a {
  /* Links do not inherit color by default in most browsers */
  color: inherit;
}

.reset-padding {
  padding: inherit;
}

A very common use of inherit is on anchor elements. Browsers apply a default color to <a> tags that overrides the inherited text color, so setting color: inherit on links inside a component ensures they match the surrounding text color without an explicit value.

inherit is one of the CSS-wide keywords available on every property, alongside initial (resets to the property’s default value), unset (either inherits or resets depending on whether the property is naturally inherited), and revert (rolls back to the browser’s default stylesheet value). Together these keywords give precise control over how the cascade resolves a property’s value.

initial

The initial keyword resets a CSS property to its initial value as defined in the CSS specification — the value the property has before any author or user stylesheet is applied. It is one of the CSS-wide keywords available on every property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.reset-color {
  color: initial; /* Resets to black in most cases */
}

.reset-display {
  display: initial; /* Resets to 'inline' */
}

.reset-font-weight {
  font-weight: initial; /* Resets to 'normal' (400) */
}

initial is particularly useful in component-level resets where you want to return a property to a known baseline without knowing — or being able to predict — what value has been inherited or set by earlier rules.

It is important to understand the difference between initial and inherit. initial uses the specification-defined default, which is not necessarily what the browser’s default stylesheet sets. For example, display: initial gives inline, not block, even for elements like <div> that browsers render as block by default — because the CSS specification’s initial value for display is inline.

The related keyword revert is often more useful in practice: it resets the property to the value the browser’s default stylesheet would apply, rather than the specification’s abstract initial value.

Input selectors

CSS provides a rich set of pseudo-classes specifically for styling form inputs based on their state and validity. These allow form controls to respond visually to user interaction and data validation without JavaScript.

 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
input:focus {
  outline: 2px solid var(--color-primary);
}

input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

input:required {
  border-left: 3px solid var(--color-accent);
}

input:valid {
  border-color: green;
}

input:invalid {
  border-color: red;
}

input:placeholder-shown {
  font-style: italic;
}

input:checked {
  accent-color: var(--color-primary);
}

The key input-related pseudo-classes are :focus, :focus-visible, :disabled, :enabled, :checked, :required, :optional, :valid, :invalid, :in-range, :out-of-range, :read-only, :read-write, and :placeholder-shown.

:focus-visible is particularly important — it applies focus styles only when the browser determines they are needed for accessibility (keyboard navigation), suppressing them for mouse clicks that would otherwise show a distracting ring on every clicked input.

:user-valid and :user-invalid are newer additions that only apply validation styles after the user has interacted with the field, preventing the jarring experience of seeing an input marked as invalid before the user has had a chance to type anything.

letter-spacing

The letter-spacing property adds or removes space between characters in text. Positive values increase the spacing, negative values reduce it. The value is added on top of any kerning information in the font, rather than replacing it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.headline {
  letter-spacing: -0.02em;
}

.label {
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.tight {
  letter-spacing: -0.5px;
}

Using em units for letter-spacing is generally preferred over fixed lengths because the spacing scales proportionally with the font size. This is particularly important for responsive designs where font sizes vary.

Uppercase text is a common application for positive letter-spacing — tightly spaced capitals can feel cramped, and a small tracking increase (around 0.05em to 0.15em) significantly improves legibility for labels, captions, and headings set in all-caps. Conversely, large display type often benefits from slightly negative tracking to appear more naturally spaced.

Note that letter-spacing also adds space after the last character in a line, which can cause visual alignment issues in centred text. This is a known limitation with no direct CSS fix, though it is rarely noticeable at typical tracking values.

line-height

The line-height property sets the height of a line box. It controls the vertical space between lines of text and is one of the most important properties for readability. Values can be a unitless number, a length, a percentage, or the normal keyword.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
body {
  line-height: 1.6;
}

h1 {
  line-height: 1.1;
}

.compact {
  line-height: 1.3;
}

Using a unitless number is strongly recommended over length or percentage values. A unitless value like 1.6 means “1.6 times the element’s font-size”, and crucially, it is inherited as the multiplier itself — not as a computed pixel value. This means child elements with different font sizes will calculate their own appropriate line height from the same multiplier.

If a length or percentage is used instead, the computed value (in pixels) is what gets inherited, which often produces too-tight or too-loose line spacing on child elements with different font sizes.

Body text typically reads well at 1.41.7. Headings, which are larger and usually shorter, tend to look better at 1.01.3. The lh and rlh units allow other properties to reference the computed line height directly, enabling precise vertical rhythm systems.

List style

The list-style shorthand sets the marker appearance of list items, combining list-style-type, list-style-position, and list-style-image in a single declaration. It applies to elements with display: list-item, including <li> elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ul {
  list-style: disc outside;
}

ol {
  list-style: decimal inside;
}

.custom-list {
  list-style: url('/icons/checkmark.svg') outside;
}

.no-bullets {
  list-style: none;
}

list-style-type controls the marker character — common values include disc, circle, square for unordered lists and decimal, lower-alpha, lower-roman for ordered ones. It also accepts a string value for a custom character: list-style-type: '→'.

list-style-position: outside (default) places the marker in the margin, outside the text flow. inside places it within the content box, causing wrapped lines to align under the marker rather than the text start.

The ::marker pseudo-element provides more precise styling of the marker itself — color, font-size, content, and a limited set of other properties — without affecting the list item’s content. This is the modern approach when the marker needs distinct styling rather than replacement.

margin

The margin shorthand sets the outer spacing on all four sides of an element, outside its border. It accepts one to four values following the standard shorthand pattern: one value sets all sides, two values set top/bottom and left/right, three values set top, left/right, and bottom, and four values set top, right, bottom, and left individually.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.section {
  margin: 2rem 0;        /* 2rem top/bottom, 0 left/right */
}

.card {
  margin: 1rem auto;     /* 1rem top/bottom, auto left/right */
}

.flush-top {
  margin-top: 0;
}

Setting margin: 0 auto on a block element with a defined width is the classic way to centre it horizontally within its container — the auto values distribute the available space equally on both sides.

Margins on block elements in the block direction (vertically in horizontal writing modes) collapse: when two block margins are adjacent, only the larger of the two applies rather than their sum. This is called margin collapsing, and it affects top and bottom margins between sibling elements, between a parent and its first/last child, and on empty elements. Margins in flex and grid containers do not collapse.

The logical property equivalents — margin-block and margin-inline — set margins relative to the writing direction, adapting automatically for vertical or right-to-left text flows.

Media queries

Media queries allow CSS rules to be applied conditionally based on characteristics of the device or environment — most commonly the viewport width, but also screen resolution, color capability, user preferences, and more. They are the foundation of responsive web design.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Mobile-first: base styles apply to all sizes */
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

@media (width >= 640px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (width >= 1024px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

The modern range syntax (width >= 640px) is cleaner than the older min-width form and supports intuitive range expressions like (640px <= width <= 1024px).

Beyond width, media queries can target print output, user preferences, and display capabilities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@media print {
  .no-print { display: none; }
}

@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 (hover: hover) {
  .button:hover { background: var(--color-hover); }
}

Container queries have taken over many use cases that previously required media queries for component-level responsive behaviour, but media queries remain the appropriate tool for viewport-level layout changes and user preference adaptation.

Named colors

CSS defines a set of named color keywords that can be used anywhere a color value is accepted. There are 148 named colors in the CSS specification, ranging from common names like red, blue, and white to more evocative ones like rebeccapurple, papayawhip, and cornflowerblue. All named colors are case-insensitive.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.warning {
  background-color: gold;
  color: darkred;
}

.divider {
  border-color: lightgray;
}

.highlight {
  background-color: cornsilk;
}

Two special named color keywords deserve mention. transparent is fully transparent black (rgb(0 0 0 / 0)) and is useful for resetting backgrounds, creating fade effects in gradients, or hiding borders while preserving their spacing. currentcolor is a dynamic value that resolves to the element’s computed color value, allowing other properties to inherit the text color without explicitly repeating it.

Named colors are defined in the sRGB color space and have fixed values. For production design work they are most useful for quick prototyping and utility values like transparent, currentcolor, and white/black — precise color palettes are better expressed with hex, rgb(), hsl(), or oklch() for full control and wider gamut support.

opacity

The opacity property sets the transparency of an element and all of its contents as a group. It accepts a value between 0 (fully transparent) and 1 (fully opaque), or a percentage equivalent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.disabled {
  opacity: 0.4;
}

.overlay {
  opacity: 0.8;
}

.fade-in {
  opacity: 0;
  transition: opacity 0.3s ease;
}

.fade-in.visible {
  opacity: 1;
}

A key characteristic of opacity is that it applies to the entire element as a composited unit — it does not apply individually to each child. This means a child element cannot be made more opaque than its parent. If you want a semi-transparent background without affecting the text or children, use a color with an alpha channel on background-color instead: background-color: rgb(0 0 0 / 0.5).

Setting opacity to any value less than 1 creates a new stacking context, which affects how the element and its descendants interact with z-index. This is one of several properties — alongside transform, filter, and will-change — that implicitly establish a stacking context as a side effect.

opacity is animatable and GPU-accelerated in most browsers, making it one of the most performant properties to transition.

padding

The padding shorthand sets the inner spacing between an element’s content and its border on all four sides. Like margin, it accepts one to four values following the shorthand pattern for top, right, bottom, and left.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.button {
  padding: 0.75rem 1.5rem;
}

.card {
  padding: 1.5rem;
}

.icon-button {
  padding: 0.5rem;
}

Unlike margins, padding cannot be negative and does not collapse. Padding is part of the element’s clickable and background-painted area — a larger padding increases the interactive target size of a button, for example, and the background color fills the padding region.

The relationship between padding and box-sizing is important: with the default box-sizing: content-box, padding is added outside the specified width and height, making the rendered element larger than its defined dimensions. With box-sizing: border-box, padding is included within the specified dimensions, which is typically easier to reason about and is a common global reset.

Logical equivalents — padding-block and padding-inline — set padding relative to the writing direction and are the preferred approach for internationalised layouts or components used in both horizontal and vertical writing modes.

Physical properties

Physical properties in CSS are those that refer to absolute directions in the viewport: top, right, bottom, left, width, and height. These are contrasted with logical properties, which use writing-mode-relative axes — block and inline — that adapt to different text directions and orientations.

1
2
3
4
5
6
7
/* Physical properties — always refer to the same sides */
.box {
  margin-top: 1rem;
  padding-left: 1.5rem;
  border-right: 1px solid #ccc;
  width: 300px;
}

Physical properties are straightforward for layouts that will only ever be consumed in a single writing direction (left-to-right, top-to-bottom). However, they require manual overriding when supporting right-to-left languages like Arabic or Hebrew, or vertical writing modes used in some East Asian typography.

Logical properties (margin-block-start, padding-inline-start, border-inline-end, inline-size, block-size) do the same job but map their values to the appropriate physical side based on the current writing-mode and direction. For internationalised projects or design systems intended to support multiple writing modes, logical properties are the more robust choice. For simple, single-direction layouts, physical properties remain clear and direct.

pointer-events

The pointer-events property controls whether an element can be the target of mouse, touch, and stylus events. Setting it to none makes the element completely transparent to pointer interaction — clicks, hovers, and taps pass through to whatever is underneath.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.overlay {
  pointer-events: none; /* Clicks pass through to elements below */
}

.disabled-button {
  pointer-events: none;
  opacity: 0.5;
}

.interactive {
  pointer-events: auto; /* Default — normal event behaviour */
}

pointer-events: none is commonly used for decorative overlay elements that should not interfere with interaction, for visually disabled controls, and during animations where click events should be suppressed. It is also used in layered UI systems where an absolutely positioned transparent layer needs to be visually present but not capture events.

A useful pattern is applying pointer-events: none to a parent while restoring it on specific children:

1
2
3
4
5
6
7
.modal-backdrop {
  pointer-events: none;
}

.modal-backdrop .modal {
  pointer-events: auto; /* The modal itself is still interactive */
}

For SVG elements, pointer-events accepts additional values beyond none and auto — such as visiblePainted, visibleFill, visibleStroke, visible, painted, fill, stroke, and all — providing fine-grained control over which parts of an SVG shape respond to events.

Note that pointer-events: none does not affect keyboard focus or accessibility — elements remain in the tab order and can still be activated via keyboard unless explicitly addressed.

Position

The position property establishes how an element is placed in a document and determines whether it creates a new positioning context for descendants. Five values are available: static, relative, absolute, fixed, and sticky.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.parent {
  position: relative; /* Establishes positioning context */
}

.overlay {
  position: absolute;
  inset: 0; /* Shorthand for top/right/bottom/left: 0 */
}

.nav {
  position: sticky;
  top: 0;
}

static is the default — the element follows normal document flow with no special positioning. relative keeps the element in flow but allows it to be offset from its natural position using top, right, bottom, and left, without affecting surrounding elements. It also establishes a containing block for absolutely positioned descendants.

absolute removes the element from flow and positions it relative to the nearest non-static ancestor. fixed positions relative to the viewport. sticky is a hybrid: the element remains in flow and behaves like relative until it reaches a specified scroll threshold, at which point it sticks like a fixed element within its scroll container.

The inset shorthand property sets top, right, bottom, and left in one declaration, following the same one-to-four value pattern as margin and padding.

Relative positioning

Relative positioning moves an element from its natural position in the document flow, offset by specified values, while the space it originally occupied is preserved. Surrounding elements are not affected — the layout behaves as if the element is still in its original position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.nudged {
  position: relative;
  top: -2px;  /* Move up by 2px from natural position */
  left: 4px;
}

.badge {
  position: relative;
  top: -0.25em; /* Slight upward shift for visual alignment */
}

Because position: relative does not remove the element from flow, it is rarely used for significant layout repositioning — large offsets produce visual overlap without rearranging the surrounding content. Its most common uses are small optical adjustments and, more importantly, establishing a containing block for absolutely positioned children.

1
2
3
4
5
6
7
8
9
.card {
  position: relative; /* Makes .card the containing block for .badge */
}

.badge {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
}

Setting position: relative on a container — even without any offset values — is the standard pattern for scoping absolutely positioned descendants to that container.

rem

The rem unit (root em) is a relative length unit equal to the computed font-size of the root element — the <html> element. Unlike em, which is relative to the current element’s font size and can compound when nested, rem always references the same root value, making it consistent and predictable throughout a stylesheet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
:root {
  font-size: 16px; /* 1rem = 16px everywhere */
}

h1 {
  font-size: 2.5rem;   /* 40px */
}

p {
  font-size: 1rem;     /* 16px */
  margin-bottom: 1rem; /* 16px */
}

.small {
  font-size: 0.875rem; /* 14px */
}

Because rem is anchored to the root, it does not suffer from the compounding problem of em — a deeply nested element using rem gets the same value as a top-level element. This makes rem the preferred unit for font sizes and spacing in design systems, where consistent scale is important.

A common practice is to leave the root font-size unset (or set it to a percentage rather than an absolute value), allowing it to respect the user’s browser font size preference. Setting font-size: 62.5% on :root was historically used to make 1rem equal 10px for easier mental arithmetic, though this approach can interfere with user accessibility settings and is less favoured today.

Selectors (core)

CSS selectors identify which elements a rule’s declarations apply to. The core selector types form the building blocks for targeting any element or group of elements in a document.

Type selectors match elements by their tag name. Class selectors (.class) match elements with a given class attribute. ID selectors (#id) match a single element with a given ID. The universal selector (*) matches any element.

1
2
3
4
p { }           /* Type selector */
.card { }       /* Class selector */
#header { }     /* ID selector */
* { }           /* Universal selector */

Combinators express relationships between elements: the descendant combinator (space) matches any descendant, the child combinator (>) matches direct children only, the adjacent sibling combinator (+) matches the immediately following sibling, and the general sibling combinator (~) matches all following siblings.

1
2
3
4
article p { }       /* Any p inside article */
article > p { }     /* Direct child p of article */
h2 + p { }          /* p immediately after h2 */
h2 ~ p { }          /* All p siblings after h2 */

Attribute selectors match elements based on their attributes and values:

1
2
3
4
5
6
[href] { }                  /* Has href attribute */
[type="submit"] { }         /* Exact value match */
[class~="icon"] { }         /* Space-separated word match */
[href^="https"] { }         /* Starts with */
[href$=".pdf"] { }          /* Ends with */
[href*="example"] { }       /* Contains */

Selector lists (,) apply a rule to multiple selectors at once. Specificity — determined by the types of selectors used — controls which rule wins when multiple rules target the same element.

Static positioning

Static positioning is the default positioning behaviour for all elements. An element with position: static is placed according to the normal document flow — block elements stack vertically, inline elements flow horizontally — and the offset properties top, right, bottom, and left have no effect.

1
2
3
.element {
  position: static; /* Default — usually not written explicitly */
}

Because static is the default, it is rarely declared explicitly. The main reason to set it is to reset an element that has inherited or been given a non-static position, returning it to normal flow behaviour.

Statically positioned elements do not create a positioning context, so absolutely positioned descendants will look past them to the nearest non-static ancestor (or the initial containing block) to determine their position.

Understanding static positioning matters most when reasoning about which ancestor an absolutely positioned element is relative to — scanning up the DOM for the first element with a position value other than static identifies the containing block.

style (attribute)

The style HTML attribute allows CSS declarations to be applied directly to an individual element in the markup. Styles set this way are called inline styles and have the highest specificity of any author style — they override rules from stylesheets unless those rules use !important.

1
2
<p style="color: red; font-weight: bold;">Important notice</p>
<div style="--accent: oklch(60% 0.2 30);">...</div>

Inline styles are useful in specific situations: server-generated dynamic values that cannot be known at stylesheet authoring time (such as progress bar widths, chart colors, or user-defined theme colors), and CSS custom property values that feed into a component’s stylesheet.

1
<div class="progress-bar" style="--progress: 72%">
1
2
3
.progress-bar::after {
  width: var(--progress);
}

For general styling, inline styles are considered poor practice — they are harder to maintain, cannot be overridden without !important, cannot use pseudo-classes or pseudo-elements, and mix concerns between structure and presentation. Stylesheet-based styling is preferred for everything except genuinely dynamic, per-element values.

Inline styles can also be read and set via JavaScript with element.style, which provides programmatic access to individual CSS properties on an element.

System colors

System colors are a set of CSS color keywords that resolve to the colors used by the operating system’s UI. They allow web content to automatically adopt the user’s system color scheme — including high-contrast modes and custom themes — without explicitly querying prefers-color-scheme or hardcoding dark/light palette variants.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.button {
  background-color: ButtonFace;
  color: ButtonText;
  border: 1px solid ButtonBorder;
}

.input {
  background-color: Field;
  color: FieldText;
}

body {
  background-color: Canvas;
  color: CanvasText;
}

The CSS Color specification defines a standard set of system color keywords including Canvas, CanvasText, LinkText, ButtonFace, ButtonText, ButtonBorder, Field, FieldText, Highlight, HighlightText, AccentColor, and AccentColorText, among others.

System colors are particularly valuable for accessibility. Users who rely on high-contrast modes or custom system themes do so because the default colors are unusable for them. Components that use system colors respect those preferences automatically, whereas components with hardcoded colors may remain inaccessible even when forced colors mode is active. Using system colors is the recommended approach for ensuring custom UI components remain accessible under forced-color modes.

Text overflow

The text-overflow property controls how text that overflows its container is visually indicated when overflow is hidden. The most common value, ellipsis, replaces the clipped text with a “…” character to signal that content has been truncated. It only takes effect when the element has overflow set to a value other than visible and white-space: nowrap to prevent the text from wrapping.

1
2
3
4
5
.truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

This three-property combination is the standard pattern for single-line text truncation — a card title, a table cell, or a file name that must fit within a fixed width.

For multi-line truncation (clamping text to a set number of lines), -webkit-line-clamp is the widely supported approach:

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

text-overflow accepts two values to set the overflow indicator for the start and end of the text independently, which is useful for bidirectional text. The clip value (default) simply cuts the text off at the container edge without any indicator character.

text-overflow only affects the visual rendering — the full text content remains in the DOM and is accessible to screen readers and search engines.

text-align

The text-align property sets the horizontal alignment of inline content (text, inline elements) within a block container. The main values are left, right, center, justify, and the logical keywords start and end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
h1 {
  text-align: center;
}

p {
  text-align: start; /* Left in LTR, right in RTL */
}

.price {
  text-align: right;
}

.body-text {
  text-align: justify;
}

start and end are the logical equivalents of left and right — they adapt to the text’s writing direction (ltr or rtl) rather than referring to fixed physical sides. Using start instead of left is better practice for internationalised content.

text-align: justify stretches lines to fill the full container width by adjusting spacing between words. It is common in print design but can produce uneven word spacing in narrow containers on the web, particularly without hyphenation enabled. Pairing text-align: justify with hyphens: auto reduces the severity of large spacing gaps.

text-align is inherited, so it can be set on a container to apply to all inline content within it.

text-indent

The text-indent property indents the first line of a block of text by a specified amount. It is the CSS equivalent of a paragraph indent in word processing, and is the conventional way to mark paragraph breaks in certain typographic styles — particularly those that omit the space between paragraphs in favour of an indent.

1
2
3
p + p {
  text-indent: 1.5em;
}

Negative values produce a hanging indent, where the first line extends to the left of the text block and subsequent lines are indented:

1
2
3
4
.hanging {
  padding-left: 2rem;
  text-indent: -2rem;
}

Hanging indents are useful for definition lists, bibliographies, and any list-like content where a label or initial text should stand apart from the wrapped body.

Two optional keywords modify the behaviour: each-line applies the indent after every hard line break (not just the first line), and hanging inverts the indent so it applies to all lines except the first, which is useful shorthand for the negative + padding pattern above.

text-indent is inherited and accepts length and percentage values. Percentage values are relative to the containing block’s width.

text-shadow

The text-shadow property adds one or more shadows to text. Each shadow is defined by a horizontal offset, a vertical offset, an optional blur radius, and a color. Multiple shadows can be stacked in a comma-separated list, with the first shadow appearing on top.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.heading {
  text-shadow: 2px 2px 4px rgb(0 0 0 / 0.3);
}

.glow {
  text-shadow: 0 0 8px oklch(70% 0.3 200);
}

.outlined {
  text-shadow:
    -1px -1px 0 #000,
     1px -1px 0 #000,
    -1px  1px 0 #000,
     1px  1px 0 #000;
}

The offset values move the shadow from the text’s position: positive horizontal offsets go right, positive vertical offsets go down. The blur radius is optional (defaults to 0 for a hard-edged shadow) and controls how spread out and soft the shadow appears.

Multiple shadows can be used together to create outline effects, layered glows, or complex decorative treatments. The stacking example above uses four 1px shadows at diagonal positions to simulate a text outline — a technique predating paint-order and useful in contexts where it remains necessary.

Unlike box-shadow, text-shadow does not support an inset keyword or a spread radius.

text-transform

The text-transform property controls the capitalisation of text, converting it to uppercase, lowercase, or title case visually without changing the underlying HTML content. The transformation is purely presentational — the source text remains unchanged in the DOM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.nav-link {
  text-transform: uppercase;
}

.byline {
  text-transform: lowercase;
}

.page-title {
  text-transform: capitalize;
}

.normal {
  text-transform: none;
}

uppercase converts all characters to their uppercase equivalents. lowercase converts all to lowercase. capitalize converts the first character of each word to uppercase, leaving the rest unchanged. none is the default, applying no transformation.

Because text-transform operates on the rendered output without modifying the DOM, it is accessible — screen readers read the original source text, avoiding the awkward letter-by-letter reading that can occur when text is manually written in all caps. This makes text-transform: uppercase the recommended approach for all-caps labels and headings rather than writing them in uppercase in the HTML.

The full-width and full-size-kana values are additional options for specific East Asian typography needs.

Transitions (CSS)

CSS transitions animate the change of a property’s value from one state to another over a specified duration. When a property value changes — due to a class toggle, pseudo-class like :hover, or JavaScript — the transition interpolates smoothly between the old and new values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.button {
  background-color: #3a6bc4;
  transform: translateY(0);
  transition: background-color 0.2s ease, transform 0.2s ease;
}

.button:hover {
  background-color: #2a5bb4;
  transform: translateY(-2px);
}

The transition shorthand sets transition-property, transition-duration, transition-timing-function, and transition-delay for one or more properties. Multiple transitions are comma-separated.

1
2
3
4
5
6
.panel {
  transition:
    opacity 0.3s ease,
    transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
    background-color 0.15s ease;
}

Using transition-property: all applies the transition to every animatable property, which is convenient but can produce unexpected results and is less performant than specifying properties explicitly. transform and opacity are the most performant properties to transition as they are GPU-composited and do not trigger layout or paint.

transition-delay sets a wait time before the transition begins — useful for sequencing effects or delaying a hover state from activating immediately.

Combined with @starting-style and transition-behavior: allow-discrete, transitions can now also animate discrete properties like display and content-visibility, enabling enter and exit animations on elements that toggle in and out of the DOM.

User action pseudo-classes

User action pseudo-classes match elements based on how the user is currently interacting with them. They are the primary tool for providing interactive feedback — hover states, focus indicators, and active press effects — without JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.button:hover {
  background: var(--color-primary-hover);
}

.button:active {
  transform: scale(0.98);
}

.link:focus {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.input:focus-visible {
  outline: 2px solid var(--color-focus);
}

The four core user action pseudo-classes are :hover, :active, :focus, and :focus-visible.

:hover applies while the pointer is over the element. It should be used thoughtfully — touch devices have no hover state, so critical functionality should never depend on it.

:active applies while an element is being activated — typically during a mouse click or touch press. It is brief but provides important tactile feedback.

:focus applies when an element has received focus, either via keyboard, mouse, or touch. All interactive elements should have a visible :focus style for accessibility.

:focus-visible is the more refined version of :focus — it applies only when the browser determines a focus indicator is needed for accessibility, suppressing it for mouse/touch interactions while still showing it for keyboard navigation. It is now the recommended approach for focus styling, replacing the older practice of outline: none on :focus with mouse-click exceptions.

vertical-align

The vertical-align property controls the vertical alignment of an inline or table-cell element relative to the line box or table row it sits in. It does not apply to block-level elements — for those, flexbox or grid alignment properties are appropriate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
img {
  vertical-align: middle; /* Align image midpoint to text midpoint */
}

sup {
  vertical-align: super;
}

sub {
  vertical-align: sub;
}

.icon {
  vertical-align: -0.125em; /* Slight downward nudge */
}

td {
  vertical-align: top;
}

Keyword values include baseline (default), top, middle, bottom, text-top, text-bottom, sub, and super. Length and percentage values offset the element relative to its baseline — positive values move it up, negative values move it down.

vertical-align: middle aligns the element’s midpoint with the middle of the lowercase letters of the surrounding text (the x-height midpoint), not the exact centre of the line box. Small downward offsets with a negative length value are often needed for precise optical alignment of icons with adjacent text.

In table cells, vertical-align: top, middle, and bottom align the cell’s content within the row height — a separate use case unrelated to inline alignment.

visibility

The visibility property shows or hides an element while preserving the space it occupies in the layout. This distinguishes it from display: none, which removes the element from the layout entirely and collapses its space.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.hidden {
  visibility: hidden; /* Invisible but still occupies space */
}

.visible {
  visibility: visible;
}

.collapsed {
  visibility: collapse; /* For table rows/columns: collapses space */
}

visibility: hidden makes the element invisible and removes it from the accessibility tree — it cannot be focused or interacted with — but its box remains in the document flow, keeping surrounding elements in place. This is useful when toggling the presence of an element that other layout depends on, and you do not want a layout shift when it appears and disappears.

An important inheritance behaviour: visibility is inherited, but a child can override it. Setting visibility: hidden on a parent and visibility: visible on a child makes the child visible while the parent remains invisible — a technique for hiding a container while selectively revealing individual children.

visibility: collapse is specifically designed for table elements — it removes a row or column from the table layout without affecting the table’s column widths, similar to how display: none would work but without the column width changes. In non-table contexts it behaves like hidden.

white-space

The white-space property controls how white space characters (spaces, tabs, newlines) in the source HTML are handled, and whether text wraps to the next line when it reaches the edge of its container. It is a shorthand for the longhands white-space-collapse and text-wrap-mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.code-block {
  white-space: pre; /* Preserve all white space, no wrapping */
}

.no-wrap {
  white-space: nowrap; /* Collapse white space, prevent wrapping */
}

.pre-wrap {
  white-space: pre-wrap; /* Preserve white space, allow wrapping */
}

p {
  white-space: normal; /* Default: collapse white space, wrap */
}

normal is the default: multiple spaces and newlines in the source are collapsed to a single space, and text wraps as needed. nowrap does the same collapsing but prevents wrapping — text overflows its container rather than breaking to a new line. pre preserves all white space and newlines exactly as written, without wrapping — the behaviour of the <pre> element. pre-wrap preserves white space but still allows wrapping. pre-line collapses spaces but preserves newlines.

white-space: nowrap is commonly used on labels, navigation items, and table cells where wrapping would break a layout. pre and pre-wrap are essential for displaying code, poetry, or any content where the author’s line breaks are meaningful.

Width and height

The width and height properties set the dimensions of an element’s content area (or border area, when box-sizing: border-box is set). They accept length values, percentages, the auto keyword, and intrinsic size keywords.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.fixed {
  width: 320px;
  height: 200px;
}

.fluid {
  width: 100%;
  max-width: 60ch;
}

.square {
  width: 4rem;
  height: 4rem;
}

.intrinsic {
  width: fit-content;
}

auto is the default for both properties and lets the browser calculate the size: width: auto on a block element fills the available container width, while height: auto sizes to the content. Percentage values are relative to the containing block’s corresponding dimension.

The intrinsic size keywords — min-content, max-content, and fit-content — size the element based on its content rather than the container. min-content shrinks to the smallest size without overflow, max-content expands to fit all content without wrapping, and fit-content behaves like max-content up to the available space then shrinks like min-content.

min-width, max-width, min-height, and max-height constrain the element’s dimensions within a range, and take precedence over width and height when the computed values conflict.

word-break

The word-break property controls where line breaks occur within words. It is distinct from overflow-wrap, which only breaks words as a last resort to prevent overflow — word-break can aggressively break all words at any character boundary, regardless of whether they would overflow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.normal {
  word-break: normal; /* Default: break at standard word boundaries */
}

.break-all {
  word-break: break-all; /* Break any word at any character */
}

.keep-all {
  word-break: keep-all; /* Prevent breaks in CJK text */
}

normal uses the default line breaking rules for the language. For Latin text this means breaking only at spaces and hyphens; for CJK (Chinese, Japanese, Korean) text it allows breaks between any two characters.

break-all breaks words at any character boundary to prevent overflow, regardless of the language. This is useful for long technical strings, code, or user-generated content where words might be arbitrary sequences of characters. It affects all words, not just those that overflow — which can produce awkward-looking breaks in regular text.

keep-all prevents CJK text from breaking between characters, requiring spaces or punctuation as break points — similar to the behaviour of Latin text. It has no effect on non-CJK text.

For breaking only long overflow-causing words without affecting normal word wrapping, overflow-wrap: break-word is usually more appropriate than word-break: break-all.

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 */
}

word-spacing

The word-spacing property adds or removes space between words. The value is added on top of the normal word spacing defined by the font, so word-spacing: 0 is not the same as the default normal — use normal to restore the font’s intended spacing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.spread-heading {
  word-spacing: 0.25em;
}

.tight-caption {
  word-spacing: -0.05em;
}

.reset {
  word-spacing: normal;
}

Positive values increase the space between words; negative values reduce it. Like letter-spacing, using em units is preferred because the adjustment scales proportionally with the font size.

word-spacing is most useful for display text and headings where fine-tuning the rhythm between words contributes to a specific typographic effect. For body text, significant changes to word spacing can impair readability — it is better left at normal or adjusted only very subtly. The property is also applied to spaces between inline elements, not just between text words, which is occasionally useful for spacing inline-block or inline-flex items.

word-spacing is an inherited property.

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.

z-index

The z-index property controls the stacking order of a positioned element along the z-axis — which elements appear in front of or behind others when they overlap. Higher values stack in front of lower values. It only takes effect on elements with a position value other than static, or on flex and grid items.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.modal-overlay {
  position: fixed;
  z-index: 100;
}

.modal {
  position: fixed;
  z-index: 101;
}

.tooltip {
  position: absolute;
  z-index: 10;
}

z-index values are integers and can be negative. The default value auto means the element participates in the stacking context of its parent without establishing a new one.

The most important concept for understanding z-index behaviour is the stacking context. Certain CSS properties create a new stacking context — position with a non-auto z-index, opacity less than 1, transform, filter, isolation: isolate, and several others. Within a stacking context, z-index values only compete with siblings in the same context; a child element cannot be stacked below its parent’s stacking context, regardless of how low its z-index is set.

This is the most common source of z-index confusion: an element with z-index: 9999 can still appear behind another element if it is contained within a lower-stacking-context parent. Using isolation: isolate on a component creates a contained stacking context without side effects, preventing internal z-index values from interfering with the rest of the page.