🧮CSS Functions

css

A reference of modern CSS functions — the ones worth knowing about beyond the basics like rgb(), calc(), and var().

Colour

color-mix()

Mix two colours together in a given colour space. The percentage controls how much of the second colour is mixed in.

.card {
  /* 50/50 mix */
  background: color-mix(in srgb, royalblue, white);

  /* 25% white into royalblue = a lighter blue */
  background: color-mix(in srgb, royalblue, white 25%);

  /* Mix in oklch for perceptually uniform results */
  background: color-mix(in oklch, #e63946, #457b9d);

  /* Create a semi-transparent version of a colour */
  background: color-mix(in srgb, currentColor, transparent 50%);

  /* Works with custom properties */
  --primary: royalblue;
  background: color-mix(in srgb, var(--primary), black 20%);
}

light-dark()

Returns one of two values depending on the user’s colour scheme. Requires color-scheme to be set.

:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #1a1a1a);
  color: light-dark(#111111, #eeeeee);
}

.card {
  border: 1px solid light-dark(#ddd, #444);
  box-shadow: 0 2px 8px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
}

a {
  color: light-dark(royalblue, cornflowerblue);
}

color()

Access wider-gamut colour spaces like Display P3:

.vibrant {
  /* Standard sRGB fallback */
  background: #ff4500;
  /* P3 — more vivid on supported displays */
  background: color(display-p3 1 0.27 0);
}

.subtle {
  background: color(display-p3 0.5 0.8 0.6);
}

oklch() and oklab()

Perceptually uniform colour spaces. Great for generating palettes where lightness and chroma are predictable:

:root {
  /* oklch(lightness chroma hue) */
  --blue-50: oklch(0.95 0.05 250);
  --blue-100: oklch(0.9 0.08 250);
  --blue-200: oklch(0.8 0.12 250);
  --blue-500: oklch(0.6 0.2 250);
  --blue-900: oklch(0.3 0.12 250);

  /* Rotate hue to create related colours */
  --primary: oklch(0.6 0.2 250);
  --secondary: oklch(0.6 0.2 310);
  --accent: oklch(0.6 0.2 30);
}

Relative colour syntax

Transform an existing colour by adjusting individual channels:

:root {
  --brand: royalblue;

  /* Make it 20% lighter */
  --brand-light: oklch(from var(--brand) calc(l + 0.2) c h);

  /* Make it 20% darker */
  --brand-dark: oklch(from var(--brand) calc(l - 0.2) c h);

  /* Desaturate */
  --brand-muted: oklch(from var(--brand) l calc(c * 0.5) h);

  /* Set alpha */
  --brand-faded: oklch(from var(--brand) l c h / 0.5);

  /* Shift hue */
  --brand-complement: oklch(from var(--brand) l c calc(h + 180));
}

Math

min(), max(), clamp()

Responsive sizing without media queries:

.container {
  /* The smaller of 90vw or 1200px */
  width: min(90vw, 1200px);

  /* At least 300px wide */
  width: max(300px, 50vw);

  /* Fluid with a floor and ceiling */
  width: clamp(300px, 50vw, 1200px);
}

h1 {
  /* Fluid typography */
  font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}

.card {
  /* Fluid padding */
  padding: clamp(1rem, 3vw, 2.5rem);
}

round()

Round a value to the nearest interval:

.grid-item {
  /* Snap width to nearest 100px */
  width: round(33.3vw, 100px);

  /* Round down */
  width: round(down, 33.3vw, 50px);

  /* Round up */
  width: round(up, 33.3vw, 50px);
}

abs() and sign()

.element {
  /* Absolute value */
  margin-top: abs(-2rem); /* 2rem */

  /* sign() returns -1, 0, or 1 */
  --direction: sign(-50px); /* -1 */
  transform: translateX(calc(var(--direction) * 100px));
}

rem() and mod()

.striped {
  /* rem() — remainder with the sign of the dividend */
  --offset: rem(17px, 5px); /* 2px */

  /* mod() — remainder with the sign of the divisor */
  --offset: mod(17px, 5px); /* 2px */
  --offset: mod(-17px, 5px); /* 3px (different from rem) */
}

Trig functions

.dial {
  --angle: 45deg;
  --radius: 100px;

  /* Position a point on a circle */
  --x: calc(cos(var(--angle)) * var(--radius));
  --y: calc(sin(var(--angle)) * var(--radius));
  transform: translate(var(--x), var(--y));
}

.element {
  transform: rotate(atan2(100px, 200px));
}

pow(), sqrt(), log(), exp()

:root {
  /* Type scale using pow() */
  --step-0: 1rem;
  --step-1: calc(1rem * pow(1.25, 1)); /* 1.25rem */
  --step-2: calc(1rem * pow(1.25, 2)); /* 1.5625rem */
  --step-3: calc(1rem * pow(1.25, 3)); /* 1.953rem */

  --hypotenuse: calc(1px * sqrt(pow(3, 2) + pow(4, 2))); /* 5px */
}

Layout & sizing

repeat()

Create repeating grid tracks:

.grid {
  /* 3 equal columns */
  grid-template-columns: repeat(3, 1fr);

  /* Auto-fill — as many 200px columns as fit */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

  /* Auto-fit — same but collapses empty tracks */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

  /* Complex patterns */
  grid-template-columns: repeat(3, 1fr 2fr);
  /* = 1fr 2fr 1fr 2fr 1fr 2fr */
}

minmax()

Set a size range for grid tracks:

.layout {
  grid-template-columns: minmax(200px, 300px) 1fr;
  grid-template-rows: minmax(100px, auto);

  /* Prevent content blow-out with min() inside minmax() */
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
}

fit-content()

Size a track to its content up to a maximum:

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

.label {
  width: fit-content(100%);
}

Selectors & filters

:is()

Matches any selector in the list. Takes the specificity of the most specific argument:

/* Instead of: header a, nav a, footer a */
:is(header, nav, footer) a {
  color: royalblue;
}

/* Deeply nested — great for reducing repetition */
:is(h1, h2, h3, h4) :is(a, span) {
  text-decoration: underline;
}

:where()

Same as :is() but with zero specificity — great for default styles that are easy to override:

/* Base styles with no specificity baggage */
:where(ul, ol) {
  padding-left: 1.5rem;
}

:where(.card) :where(h2, h3) {
  margin-top: 0;
}

/* Easily overridden with any class */
.my-list {
  padding-left: 0;
}

:has()

The “parent selector” — style an element based on what it contains:

/* Card that contains an image */
.card:has(img) {
  padding: 0;
}

/* Form group when its input is invalid */
.form-group:has(:invalid) {
  border-color: red;
}

/* Style the parent based on a checked child */
.option:has(input:checked) {
  background: lavender;
}

/* Style a sibling */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* Page-level layout based on sidebar presence */
body:has(.sidebar) .main {
  grid-template-columns: 300px 1fr;
}

:not()

Exclude elements matching a selector:

/* All links except those with a class */
a:not([class]) {
  text-decoration: underline;
}

/* All children except the last */
li:not(:last-child) {
  border-bottom: 1px solid #eee;
}

/* Combine with :has() */
.card:not(:has(img)) {
  background: #f5f5f5;
}

Shapes & gradients

conic-gradient()

.pie {
  background: conic-gradient(royalblue 0% 70%, #eee 70% 100%);
  border-radius: 50%;
}

.colour-wheel {
  background: conic-gradient(in oklch, red, yellow, lime, aqua, blue, magenta, red);
  border-radius: 50%;
}

linear-gradient() with interpolation

.smooth {
  /* Interpolate in oklch for smoother gradients */
  background: linear-gradient(in oklch, royalblue, coral);

  /* Avoid the grey-zone between complementary colours */
  background: linear-gradient(in oklch longer hue, blue, red);
}

Transform

Individual transform functions

.element {
  /* These are now individual properties, not just functions */
  translate: 50% -20px;
  rotate: 15deg;
  scale: 1.1;

  /* Still works as functions inside transform */
  transform: rotate3d(1, 1, 0, 45deg) perspective(500px);
}

Misc

env()

Access environment variables like safe area insets:

.app-bar {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.bottom-nav {
  padding-bottom: env(safe-area-inset-bottom, 1rem); /* fallback */
}

attr() (enhanced)

Reference HTML attributes as CSS values (limited support — currently only works in content, broader support coming):

.tooltip::after {
  content: attr(data-tooltip);
}

/* Future: typed attr() in any property */
/* div { width: attr(data-width px, 100px); } */

counter() and counters()

ol {
  counter-reset: section;

  li {
    counter-increment: section;

    &::before {
      content: counter(section) '. ';
      font-weight: bold;
    }
  }
}

/* Nested counters with a separator */
ol ol li::before {
  content: counters(section, '.') ' ';
}