All skills
ccheney avatar

/modern-css

@f71635f

Implement or debug CSS layouts, responsive styles, themes, and motion. Use when choosing native CSS features or replacing legacy styling with browser-compatible CSS; not for unrelated frontend logic.

Use this Skill: https://skilld.dev/gh/ccheney/robust-skills/modern-css

This session only. Nothing lands on disk.

referencesTOKENS.md

≈5.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Tokens — Design System Primitives and CSS Logic

Sources: CSS Custom Properties for Cascading Variables Level 2, CSS Properties and Values API (@property), CSS Values and Units Level 5 (if(), sibling-index(), attr()), CSS Functions and Mixins Level 1 (@function), CSS Values and Units Level 4 (math functions).

This reference covers the primitives that power design systems in pure CSS: custom properties, typed registration, custom functions, conditional values, math, positional functions, and typed attribute access. Every section explains WHEN and WHY to reach for each feature.

Custom properties, @property, and all math functions are Baseline. @function, if(), sibling-index()/sibling-count(), and typed attr() are NOT Baseline as of mid-2026 — each section notes current support.

Contents


Custom Properties (--*)

Custom properties are the foundational design token mechanism. They cascade, inherit, respond to media queries, and JavaScript can read and write them at runtime. Use them for every design token: colors, spacing, typography scales, component variants, animation parameters.

Scoping — Any Selector, Not Just :root

Scope properties to the narrowest context that makes sense.

/* ❌ Everything on :root — flat, no encapsulation */
:root {
  --card-padding: 1.5rem;
  --card-bg: #fff;
  --button-padding: 0.5rem 1rem;
  --button-bg: blue;
}

/* ✅ Scoped to the component that uses them */
.card {
  --_padding: 1.5rem;
  --_bg: #fff;
  padding: var(--_padding);
  background: var(--_bg);
}

Convention: prefix private/internal properties with --_. Public properties (the ones consumers override) use the component name: --card-bg.

Cascade and Theming

Properties set on a parent are inherited by all descendants unless overridden. This is how theming works — override semantic tokens in a context, and all components within adapt.

:root {
  --color-primary: oklch(0.6 0.2 260);
  --color-surface: oklch(0.98 0 0);
  --space-md: 1rem;
}

.dark-section {
  --color-primary: oklch(0.8 0.15 260);
  --color-surface: oklch(0.15 0 0);
}

/* Component uses tokens without knowing the theme */
.card {
  background: var(--color-surface);
  padding: var(--space-md);
}

Fallback Values

var() accepts a fallback as the second argument. Used only when the property is not set (guaranteed invalid) — not when set to an invalid value.

.element {
  color: var(--text-color, black);
  padding: var(--component-padding, var(--space-md, 1rem));
  width: var(--custom-width, calc(100% - 2rem));
}

Component Variants

Define the public API of a component with custom properties. Consumers change the properties, not the internals.

.button {
  --button-bg: var(--color-primary);
  --button-color: white;
  --button-size: var(--space-md);

  background: var(--button-bg);
  color: var(--button-color);
  padding: var(--button-size) calc(var(--button-size) * 2);
}

.button.danger { --button-bg: var(--color-danger); }
.button.ghost  { --button-bg: transparent; --button-color: var(--color-primary); }
.button.small  { --button-size: var(--space-sm); }

Runtime Manipulation via JavaScript

const root = document.documentElement;

// Read
getComputedStyle(root).getPropertyValue('--color-primary').trim();

// Write (affects all descendants)
root.style.setProperty('--color-primary', 'oklch(0.7 0.25 300)');

// Scoped override
card.style.setProperty('--card-bg', 'oklch(0.95 0.02 200)');

// Remove override (revert to inherited)
card.style.removeProperty('--card-bg');

// Dynamic values (mouse position, scroll, etc.)
document.addEventListener('pointermove', (e) => {
  root.style.setProperty('--mouse-x', `${e.clientX}px`);
  root.style.setProperty('--mouse-y', `${e.clientY}px`);
});

@property — Typed Custom Properties

Unregistered custom properties are strings. The browser cannot interpolate them during transitions — values snap instantly. Register with @property to give a type, initial value, and inheritance behavior.

WHEN to Use

  1. Animate a custom property — the primary use case. Without registration, --hue: 0 to --hue: 360 snaps. With syntax: "<number>", it interpolates smoothly.
  2. Enforce a type — prevent invalid values from propagating silently.
  3. Set a guaranteed initial value — applies even when no ancestor sets the property.
  4. Control inheritance — inherits: false prevents a token from leaking into children.

Syntax

@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.element {
  --hue: 0;
  background: oklch(0.7 0.2 var(--hue));
  transition: --hue 0.5s ease;
}
.element:hover { --hue: 120; }

Supported Syntax Types

Syntax Accepts Example initial-value
<color> Any CSS color oklch(0.5 0.2 260)
<length> Length with units 0px
<number> Unitless number 0
<percentage> Percentage value 0%
<length-percentage> Length or percentage 0px
<angle> Angle value 0deg
<time> Time value 0s
<integer> Whole number 0
<resolution> Resolution value 1dppx
<transform-function> Single transform rotate(0deg)
<transform-list> Multiple transforms rotate(0deg) scale(1)
<custom-ident> Custom identifier none
<image> Image value n/a (cannot set initial-value)
<url> URL value n/a (cannot set initial-value)
* Any value (universal) (no animation benefit)

Combine with | for union types: syntax: "<length> | <percentage>". Append + for space-separated lists: syntax: "<length>+". Append # for comma-separated lists: syntax: "<color>#".

Limitation: initial-value Must Be Computationally Independent

The initial value cannot use relative units or depend on context.

/* ❌ REJECTED — relative units depend on context */
@property --spacing {
  syntax: "<length>";
  inherits: false;
  initial-value: 2em;      /* depends on font-size */
}

/* ✅ Valid — absolute units only */
@property --spacing {
  syntax: "<length>";
  inherits: false;
  initial-value: 16px;
}

Animating Gradients (Classic Use Case)

Gradients cannot be animated directly. Register the individual values and animate those.

@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --gradient-color {
  syntax: "<color>";
  inherits: false;
  initial-value: oklch(0.6 0.25 260);
}

.gradient-bg {
  background: linear-gradient(var(--gradient-angle), var(--gradient-color), transparent);
  transition: --gradient-angle 1s ease, --gradient-color 0.5s ease;
}
.gradient-bg:hover {
  --gradient-angle: 180deg;
  --gradient-color: oklch(0.8 0.15 140);
}

Non-Inheriting Properties for Component Isolation

@property --variant {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: default;
}

.alert { --variant: warning; }
.alert .button {
  /* --variant is "default" here, not "warning" — isolation works */
}

@function — Custom CSS Functions

Chromium-only (Chrome/Edge 139+) as of mid-2026. Not Baseline. Unsupported browsers ignore the @function rule AND any declaration calling a --fn() — so always provide a fallback declaration before the one using the function.

Define reusable computations that return a value. They accept parameters, support defaults, and are cascade-aware (last definition in the same layer wins).

WHEN to Use

Use when a computation is used in multiple places and needs to stay consistent: fluid typography, spacing scales, opacity variants, contrast-aware colors.

Basic Syntax

@function --double(--value) {
  result: calc(var(--value) * 2);
}

.element {
  padding: --double(1rem);   /* 2rem */
  gap: --double(0.5rem);     /* 1rem */
}

Parameters with Types and Defaults

@function --fluid-size(
  --min type(<length>),
  --max type(<length>),
  --min-vw type(<length>): 20rem,
  --max-vw type(<length>): 80rem
) {
  result: clamp(
    var(--min),
    var(--min) + (var(--max) - var(--min)) *
      (100vw - var(--min-vw)) / (var(--max-vw) - var(--min-vw)),
    var(--max)
  );
}

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

Opacity Helper

@function --opacity(--color type(<color>), --alpha type(<percentage>): 100%) {
  result: color-mix(in oklch, var(--color) var(--alpha), transparent);
}

.overlay { background: --opacity(var(--color-primary), 50%); }
.subtle-border { border: 1px solid --opacity(var(--color-text), 20%); }

Conditional Logic Inside Functions

Functions can contain @media and other at-rules.

@function --theme-value(--light type(<color>), --dark type(<color>)) {
  result: var(--light);

  @media (prefers-color-scheme: dark) {
    result: var(--dark);
  }
}

.card {
  background: --theme-value(#ffffff, #1a1a2e);
  color: --theme-value(#1a1a2e, #e0e0e0);
}

Cascade Awareness

Override a function in a later layer or later in the source.

@layer base {
  @function --spacing(--n type(<number>)) {
    result: calc(var(--n) * 0.25rem);    /* 4px base */
  }
}

@layer theme {
  @function --spacing(--n type(<number>)) {
    result: calc(var(--n) * 0.5rem);     /* 8px base — overrides */
  }
}

.element { padding: --spacing(4); }  /* 2rem (uses theme layer) */

if() — Conditional Values

Chromium-only (Chrome/Edge 137+) as of mid-2026. Not Baseline. Feature-detect against a real property, e.g. @supports (color: if(style(--x): red; blue)). Do NOT test with a custom property (@supports (--x: if(...))) — custom properties accept any value, so that test is always true.

Evaluates conditions inline and returns the matching value. Reduces the need for multiple @media or @container blocks when varying a single property.

Condition Types

Condition Tests Example
media() Media query media(width >= 768px)
supports() Feature support supports(display: grid)
style() Custom property value style(--variant: compact)

CRITICAL: style() Checks the SAME Element

Container style queries require the condition on an ancestor container. if(style(...)) checks the property on the element itself.

/* ❌ Container style query — needs an ancestor with the property */
.card-wrapper { --variant: compact; }
@container style(--variant: compact) {
  .card { padding: 0.5rem; }
}

/* ✅ if() — checks the element's own property */
.card {
  --variant: default;
  padding: if(style(--variant: compact): 0.5rem; 1.5rem);
}
.card.compact { --variant: compact; /* padding becomes 0.5rem */ }

Syntax Rules

No space between if and (. Condition followed by :, truthy value, ;, falsy value.

.element {
  display: if(media(width < 640px): none; block);
  gap:     if(media(width >= 1024px): 2rem; 1rem);
}

Chained Conditions

.heading {
  font-size: if(
    media(width >= 1200px): 3rem;
    media(width >= 768px): 2rem;
    1.25rem
  );
}

Boolean Custom Property Pattern

.accordion-panel {
  --expanded: false;
  max-height: if(style(--expanded: true): 500px; 0);
  overflow: hidden;
  transition: max-height 0.3s ease;
}
.accordion-panel[open] { --expanded: true; }

CSS Math Functions

Math functions eliminate preprocessor arithmetic and enable computations that respond to runtime values. They nest freely inside each other.

Core: calc(), min(), max(), clamp()

Baseline — use freely everywhere.

.sidebar { width: calc(100% - var(--sidebar-width)); }

/* min() = ceiling, max() = floor */
.container { width: min(90%, 1200px); }
.element   { font-size: max(1rem, 2vw); }

/* clamp(minimum, preferred, maximum) */
.fluid-text {
  font-size: clamp(1rem, 2.5vw, 2rem);
}
.container {
  width: clamp(320px, 90%, 1200px);
  padding-inline: clamp(1rem, 5vw, 3rem);
}

WHEN: clamp() for fluid typography/spacing. min() to cap a maximum. max() to enforce a minimum. calc() for mixed-unit arithmetic.

Stepped: round(), mod(), rem()

Baseline. Snap values to grids or extract remainders.

/* round(strategy, value, interval) */
.element {
  width: round(nearest, 100vw, 100px);
  height: round(up, var(--content-height), 8px);
  padding: round(down, 2.7rem, 0.25rem);   /* 2.5rem */
}

/* mod() for cyclic patterns */
.grid-item { --offset: mod(var(--index), 3); }

WHEN: round() to align to a spacing grid (4px, 8px baseline). mod() for repeating patterns.

Sign: abs(), sign()

Baseline.

.element {
  --distance: abs(var(--position) - 50%);
  transform: scaleX(sign(var(--velocity)));
}

WHEN: abs() for magnitude regardless of direction. sign() to extract direction for transforms.

Trigonometric: sin(), cos(), tan(), asin(), acos(), atan(), atan2()

Baseline. Accept angles, return unitless numbers.

/* Circular layout */
.circular-menu > * {
  --angle: calc(360deg / var(--items) * var(--index));
  position: absolute;
  left: calc(50% + var(--radius) * cos(var(--angle)));
  top: calc(50% + var(--radius) * sin(var(--angle)));
  translate: -50% -50%;
}

/* Wave effect */
.wave-item {
  transform: translateY(calc(sin(var(--index) * 45deg) * 20px));
}

WHEN: Circular/radial layouts, wave animations, polar coordinate systems.

Exponential: pow(), sqrt(), hypot(), log(), exp()

Baseline.

/* Modular type scale */
.heading {
  --ratio: 1.25;
  --level: 3;
  font-size: calc(1rem * pow(var(--ratio), var(--level)));
}

/* Distance calculation */
.element {
  --distance: hypot(var(--dx), var(--dy));
}

WHEN: pow() for exponential type/spacing scales. hypot() for distance. These replace Sass functions or JavaScript.


sibling-index() / sibling-count()

Not Baseline as of mid-2026: Chrome/Edge 138+ and Safari 26.2+; no Firefox. Feature-detect against a real property: @supports (transition-delay: calc(sibling-index() * 1ms)). (Testing via a custom property is always true — see the if() note above.)

Return the 1-based index and total count of siblings as integers usable in calc() — unlike counter() which returns strings only for content.

WHEN to Use

Use when you need positional styling that would otherwise require inline style="--i: N" attributes, :nth-child() rules for every position, or JavaScript.

Staggered Animations

/* ❌ Before: manually set index via HTML or nth-child */
.list-item:nth-child(1) { transition-delay: 0ms; }
.list-item:nth-child(2) { transition-delay: 50ms; }
.list-item:nth-child(3) { transition-delay: 100ms; }

/* ✅ After: single rule, any number of items */
.list-item {
  transition: opacity 0.3s ease, transform 0.3s ease;
  transition-delay: calc((sibling-index() - 1) * 50ms);
}

Dynamic Spacing and Opacity

.thread-reply {
  padding-inline-start: calc(sibling-index() * 1rem);
}

.notification {
  opacity: calc(1 - (sibling-index() - 1) / sibling-count());
}

Positional Color Shifts

.color-strip > * {
  --hue: calc(360 / sibling-count() * (sibling-index() - 1));
  background: oklch(0.7 0.2 var(--hue));
}

Circular Layout (Combined with Trig)

.radial-menu > * {
  --angle: calc(360deg / sibling-count() * (sibling-index() - 1));
  position: absolute;
  left: calc(50% + 100px * cos(var(--angle)));
  top: calc(50% + 100px * sin(var(--angle)));
  translate: -50% -50%;
}

Typed attr()

Chromium-only (Chrome/Edge 133+) as of mid-2026. Not Baseline. Feature-detect against a real property: @supports (width: attr(data-x type(<length>), 0px)).

Standard attr() only works in content and returns strings. Typed attr() parses HTML attributes as real CSS values — colors, numbers, lengths — usable in any property.

Syntax

attr(<attribute-name> type(<syntax>), <fallback>)

WHEN to Use

Use when data-driven styling makes more sense than classes: star ratings, progress bars, dynamic counts, view-transition-names from data attributes, or avoiding generated CSS for every possible value.

Data-Driven Grid Columns

/* ❌ Before: a class for every possible column count */
.grid.cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }

/* ✅ After: one rule, driven by the attribute */
.grid {
  grid-template-columns: repeat(attr(data-cols type(<number>), 1), 1fr);
}
<div class="grid" data-cols="3">...</div>

Star Ratings

.star-rating {
  --rating: attr(data-rating type(<number>), 0);
  background: linear-gradient(
    to right,
    gold calc(var(--rating) / 5 * 100%),
    #ddd calc(var(--rating) / 5 * 100%)
  );
  mask-image: url('stars.svg');
}
<div class="star-rating" data-rating="3.5"></div>

Progress Bar

.progress-bar {
  --pct: attr(data-value type(<number>), 0);
  background: linear-gradient(
    to right,
    var(--color-primary) calc(var(--pct) * 1%),
    var(--color-surface) calc(var(--pct) * 1%)
  );
}

Dynamic View Transition Names

.product-card {
  view-transition-name: attr(data-product-id type(<custom-ident>), none);
}
<div class="product-card" data-product-id="shoe-42">...</div>

Color and Spacing from Attributes

.swatch {
  background: attr(data-color type(<color>), gray);
}

.spacer {
  height: attr(data-size type(<length>), 1rem);
}
<span class="swatch" data-color="oklch(0.7 0.25 150)"></span>
<div class="spacer" data-size="2rem"></div>

Token Architecture Pattern

How these primitives compose into a design system — no frameworks, no build steps.

/* Layer 1: Primitive tokens (raw values) */
:root {
  --blue-50: oklch(0.97 0.01 250);
  --blue-500: oklch(0.6 0.2 250);
  --blue-900: oklch(0.25 0.08 250);
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-8: 2rem;
}

/* Layer 2: Semantic tokens (meaning aliases) */
:root {
  --color-primary: var(--blue-500);
  --color-bg: var(--blue-50);
  --color-text: var(--blue-900);
  --space-sm: var(--space-2);
  --space-md: var(--space-4);
  --space-lg: var(--space-8);
}

/* Layer 3: Component tokens (scoped) */
.card {
  --_padding: var(--space-md);
  --_bg: var(--color-bg);
  padding: var(--_padding);
  background: var(--_bg);
}

/* Dark theme — override semantic tokens only */
@media (prefers-color-scheme: dark) {
  :root {
    --color-primary: var(--blue-50);
    --color-bg: var(--blue-900);
    --color-text: var(--blue-50);
  }
}

Animate an entire palette by registering a hue token with @property:

@property --theme-hue {
  syntax: "<number>";
  inherits: true;
  initial-value: 250;
}

:root {
  --theme-hue: 250;
  --color-primary: oklch(0.6 0.2 var(--theme-hue));
  transition: --theme-hue 0.6s ease;
}

[data-mood="warm"]   { --theme-hue: 30; }
[data-mood="nature"] { --theme-hue: 150; }

For non-Baseline features, always feature-detect with @supports or use progressive enhancement. Check MDN or Baseline for current browser support.

Source: SKILL.md on GitHub

1 warning15d4 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    The skill is a secure and comprehensive educational resource for modern CSS development. It provides guidance on using native browser features for layout, animation, and performance without the need for JavaScript or external libraries. No malicious behaviors, exfiltration attempts, or prompt injections were found.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

  • Runlayer7mo

    11/11 files flagged

Signed by skilld at f71635f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 3 weeks ago

README badge

README badge for ccheney/robust-skills/modern-css