Docs
Next

CSS variables

Root token variables, variables* utilities and sizing-* / theme-* scopes.

On this page

Every token becomes a CSS custom property. Systhema declares them on :root, binds the color system to data-theme, and ships classes that re-declare a set of variables on any element, so you can scope a color mode or one breakpoint's sizes to part of a page, or read a token from your own CSS.

Quick referenceLink to this section

Breakpoint and design-width variables on :root, per breakpoint (default tokens):

Variablesmmdlgxl2xl3xl
--breakpoint-current06401192119211921192
--breakpoint-sm000000
--breakpoint-md640640640640640640
--breakpoint-lg119211921192119211921192
--breakpoint-xl136613661366136613661366
--breakpoint-2xl172817281728172817281728
--breakpoint-3xl204820482048204820482048
--screen-current3907681192119211921192
--screen-sm390390390390390390
--screen-md768768768768768768
--screen-lg119211921192119211921192
--screen-xl144014401440144014401440
--screen-2xl192019201920192019201920
--screen-3xl256025602560256025602560

Classes that re-declare token variables on an element:

ClassStyles
variablesDeclares 1434 custom properties across 3 rules (themes and breakpoints).
variables-themesDeclares 873 custom properties across 3 rules (themes and breakpoints).
variables-sizingDeclares 432 custom properties.
theme-defaultDeclares 291 custom properties.
theme-darkDeclares 291 custom properties.
sizing-smDeclares 432 custom properties.
sizing-mdDeclares 432 custom properties.
sizing-lgDeclares 432 custom properties.

Root variablesLink to this section

Core always emits the :root custom properties: breakpoints, fonts, color primitives, the responsive-sizing scale with its per-breakpoint @media overrides, and the color system, bound to :root and [data-theme="<mode>"]. Any element with a data-theme attribute therefore switches the color system for its subtree, which is what the components' theme prop renders.

--breakpoint-current and --screen-current hold the min width and the design width of the active breakpoint as plain numbers. The vw utilities divide by --screen-current, and scripts can read it too. Switch the preview's viewport to watch it change:

--screen-current
import { useEffect, useState } from 'react'

export default function Demo() {
  const [screen, setScreen] = useState('')

  useEffect(() => {
    const read = () =>
      setScreen(getComputedStyle(document.documentElement).getPropertyValue('--screen-current'))
    read()
    window.addEventListener('resize', read)
    return () => window.removeEventListener('resize', read)
  }, [])

  return (
    <p className="flex items-baseline gap-3">
      <code className="text-small color-label">--screen-current</code>
      <span className="text-h3 color-heading">{screen}</span>
    </p>
  )
}

One more rule ships with the root variables: when a .header is on the page, html and body get scroll-padding-top: var(--header-height, 64px), so anchor jumps and scrollIntoView land below a sticky header instead of under it. A project with a taller or shorter header overrides --header-height.

Variable utilitiesLink to this section

theme-<mode> re-declares one color-system mode on an element and sizing-<breakpoint> one breakpoint's sizes, whatever the page around them uses. The class form and the data-theme attribute give the same colors; use the class where you can't set an attribute, for example from a CMS class field.

export default function Demo() {
  return (
    <div className="grid w-full grid-cols-2 gap-4">
      <div className="theme-default flex flex-col gap-2 rounded-lg border border-(--color-foundations-surface-border) bg-layout-main p-6">
        <p className="text-label color-label">theme-default</p>
        <p className="text-h4 color-heading">Light panel</p>
        <p className="text-small color-body">Stays light on a dark page.</p>
      </div>
      <div className="theme-dark flex flex-col gap-2 rounded-lg border border-(--color-foundations-surface-border) bg-layout-main p-6">
        <p className="text-label color-label">theme-dark</p>
        <p className="text-h4 color-heading">Dark panel</p>
        <p className="text-small color-body">Stays dark on a light page.</p>
      </div>
    </div>
  )
}

sizing-sm keeps the phone sizes inside an element at every viewport, which suits a narrow sidebar or a card that should not grow its type on desktops. Switch the preview to desktop: the first heading grows, the second keeps its phone size.

sizing-* scopes
export default function Demo() {
  return (
    <div className="flex w-full flex-col gap-6">
      <div>
        <p className="text-label color-label">Page sizing</p>
        <p className="text-h2 color-heading">Quarterly report</p>
      </div>
      <div className="sizing-sm">
        <p className="text-label color-label">sizing-sm</p>
        <p className="text-h2 color-heading">Quarterly report</p>
      </div>
    </div>
  )
}

The variables family declares whole sets at once:

  • variables declares every token variable on the element: fonts, color primitives, the color system with its data-theme modes, and the responsive sizes with their breakpoint media queries.
  • variables-themes declares only the color system and its modes.
  • variables-sizing declares only the responsive sizes and their media queries.

Use them when Systhema renders inside a page that doesn't carry its :root variables, for example a widget embedded in another site whose page doesn't load Systhema's CSS at its root. Put variables on the widget's root element and everything inside it resolves. Each class weighs over a thousand declarations, so use it on a few roots, not on repeated elements.

Referencing a variableLink to this section

Reference token variables by name in your own CSS, inside the app layer so they win over Systhema's sub-layers (see Cascade layers):

src/app/(site)/globals.css
@layer app {
  .pull-quote {
    border-left: 4px solid var(--color-foundations-decorative);
    padding-left: var(--gap-md);
    color: var(--color-typography-heading);
  }
}

In markup, Tailwind's variable shorthand reads them too: bg-(--color-foundations-surface-bg), gap-(--gap-sm). The names follow the token paths in kebab case; the color, spacing and typography references list every one.

Responsive and state variantsLink to this section

The scope classes take variants: lg:sizing-md keeps tablet sizes on desktops, md:theme-dark switches a block to dark from md up. The root variables need no variants; their breakpoint overrides are built in.

CustomizingLink to this section

The variables come from your tokens: change a value in Figma (or in customTokens in systhema.config) and the variable changes with it. To change one variable for part of the page, override it in CSS on that element, since custom properties inherit:

@layer app {
  .compact {
    --gap-md: 16px;
  }
}

The :root declarations and the scope classes are always emitted. SysthemaConfig.blocks declares rootVariables and variablesUtilities toggles, but core doesn't read them, so setting either to false changes nothing.