Learn

Variants

A variant is a name that maps to a CSS selector or @-rule. Inside a tl.create style group, an object keyed by a variant name represents conditional styles:

tl.create({
  btn: {
    background:    "blue",
    _hover:    { background: "darkblue" },     // variant: pseudo-class
    sm:        { padding: "0.5rem" },          // variant: breakpoint
    _dark:     { background: "white" },        // variant: parent selector
    _disabled: { opacity: 0.5 },
  },
});

Variants stack — combine them by nesting:

tl.create({
  btn: {
    color: "white",
    _hover: {
      color: "lightblue",
      _dark: { color: "yellow" },               // dark + hover
    },
    sm: {
      _hover: { color: "darkblue" },            // small-screen hover
    },
  },
});

Built-in variants

There are 76 built-in variants in five categories. Source of truth: src/compiler/variants.ts BUILT_IN_VARIANTS.

Pseudo-classes (23)

NameSelectorDescription
_hover:hoverMouse hover
_focus:focusFocus (any source)
_focusWithin:focus-withinFocus on a descendant
_focusVisible:focus-visibleVisible-focus indicator
_active:activeMouse pressed
_visited:visitedVisited link
_disabled:disabledDisabled element
_enabled:enabledEnabled element
_checked:checkedChecked checkbox/radio
_indeterminate:indeterminateIndeterminate state
_required:requiredRequired form field
_optional:optionalOptional form field
_valid:validValid input
_invalid:invalidInvalid input
_readOnly:read-onlyRead-only
_first:first-childFirst child
_last:last-childLast child
_firstOfType:first-of-typeFirst of its type
_lastOfType:last-of-typeLast of its type
_only:only-childOnly child
_odd:nth-child(odd)Odd children
_even:nth-child(even)Even children
_empty:emptyNo children

Pseudo-elements (11)

NameSelectorDescription
_placeholder::placeholderInput placeholder
_before::before::before pseudo-element
_after::after::after pseudo-element
_selection::selectionHighlighted selection
_marker::markerList marker
_backdrop::backdropDialog/fullscreen backdrop
_fileSelectorButton::file-selector-button<input type="file"> button
_firstLetter::first-letterFirst letter
_firstLine::first-lineFirst line
_targetText::target-textURL-fragment-highlighted text
_detailsContent::details-content<details> content area

Parent / ancestor selectors (10)

NameSelectorDescription
_dark:is(.dark *)Dark mode (class strategy)
_light:not(.dark) &Light mode
_rtl[dir="rtl"] &Right-to-left
_ltr[dir="ltr"] &Left-to-right
_groupHover.group:hover &Hover on a .group ancestor
_groupFocus.group:focus &Focus on a .group ancestor
_groupActive.group:active &Active on a .group ancestor
_peerHover.peer:hover ~ &Sibling .peer is hovered
_peerFocus.peer:focus ~ &Sibling .peer is focused
_peerChecked.peer:checked ~ &Sibling .peer is checked
_peerDisabled.peer:disabled ~ &Sibling .peer is disabled

Responsive breakpoints (5)

NameMin widthTailwind equivalent
sm640 pxsm:
md768 pxmd:
lg1024 pxlg:
xl1280 pxxl:
2xl1536 px2xl:

Container queries (3)

NameSelector
_containerSm@container (min-width: 480px)
_containerMd@container (min-width: 768px)
_containerLg@container (min-width: 1024px)

Special media queries (10)

NameSelector
print@media print
portrait@media (orientation: portrait)
landscape@media (orientation: landscape)
motionSafe@media (prefers-reduced-motion: no-preference)
motionReduce@media (prefers-reduced-motion: reduce)
contrastMore@media (prefers-contrast: more)
darkOS@media (prefers-color-scheme: dark)
lightOS@media (prefers-color-scheme: light)
hover@media (hover: hover)
touch@media (hover: none) and (pointer: coarse)

Custom variants

Add your own variants with tl.extend({ variants: ... }). Both runtime and compiler discover them — at build time, Pass 1 scans every file for tl.extend({ variants: {...} }) calls, merges them into a single map, and Pass 2 uses that map when transforming tl.create calls. No central config required.

// app/variants.ts
import { tl } from "traceless-style";

export const $$ = tl.extend({
  variants: {
    _tablet:        "@media (min-width: 900px)",
    _retina:        "@media (-webkit-min-device-pixel-ratio: 2)",
    _brand:         ".my-brand &",
    _hoverDark:     ":is(.dark *):hover",
  },
});

// app/Card.tsx
const $ = tl.create({
  card: {
    padding: "1rem",
    _tablet: { padding: "2rem" },     // ✓ resolves to @media (min-width: 900px)
    _brand:  { color: "gold" },       // ✓ resolves to .my-brand & { color: gold }
  },
});

If your variant uses a multi-step selector with &, the & is replaced by a unique class. If it uses a parent-style selector with no & (like :is(.dark *)), it's used as-is.

Validation rules for custom variants

Custom variant selectors are validated by validateVariant() (src/compiler/variants.ts):

  • Selector must be a non-empty string.
  • Variant key must be a valid JS identifier (or quoted with ").
  • Selector cannot contain raw ;, }, or other CSS-injection chars.
  • @media / @container / @supports rules are recognized as at-rules and emitted at the top level.

Any validation failure becomes a build error pointing at the tl.extend call site.

Stacking variants

Variants can be nested inside one another. Each level extends the selector of the parent:

tl.create({
  btn: {
    background: "blue",
    _hover:  { background: "darkblue",
      _dark: { background: "black"  },     // → :is(.dark *):hover
    },
    sm: {                                  // → @media (min-width: 640px) { … }
      _hover: { background: "navy" },      // → @media (min-width: 640px) { :hover { … } }
    },
  },
});

Order doesn't matter for the compiled CSS — each unique (property, value, selector) combination becomes one rule, regardless of where it appeared in the source tree.

Variants vs. raw selectors

Sometimes you need a one-off selector that isn't worth defining as a custom variant. Any object key starting with :, &, [, ., @, or > is treated as a raw selector pass-through:

tl.create({
  list: {
    listStyle: "none",
    "& > li:not(:first-child)": { borderTop: "1px solid #eee" },
    "@media (min-resolution: 2dppx)": { borderWidth: "0.5px" },
  },
});

Raw selectors are still validated for injection-safety, but they are not stored in the variant registry.

Continue to 6. Design tokens & themes.

See also