tl.extend — Register Custom CSS Variants Anywhere in Your Codebase, No Central Config Required

Every atomic-CSS system eventually hits the same wall: the built-in breakpoints and pseudo-classes are great — until the day you need something they never shipped with. A brand-specific selector. A retina-display query. A tablet breakpoint that doesn’t match any of the defaults. Most tools force you to crack open one central config file, edit it, and hope you don’t step on a teammate’s PR.

traceless-style solves this differently with tl.extend — and the way it solves it is the most interesting part.

What tl.extend actually does

tl.extend registers custom variants. The key detail: both the runtime and the compiler discover them automatically. At build time, a first pass scans every file in the project for tl.extend({ variants: {...} }) calls and merges everything it finds into a single map. A second pass then uses that merged map whenever it transforms a tl.create call.

The practical result: there is no central registry file to maintain. You can define a custom variant in the same component file where you use it, in a shared theme.ts, or scattered across a dozen files — the compiler finds all of them and treats them as one unified vocabulary.

Signature

function extend(options: ExtendOptions): TracelessStyleInstance;

interface ExtendOptions {
  variants: Record<string, string>;
  prefix?:  string;
}

interface TracelessStyleInstance {
  create:   typeof create;
  merge:    typeof merge;
  cx:       typeof cx;
  variants: FlatVariants;
  errors:   VariantValidationError[];
}

variants is a flat map of variant-key → selector-string. The returned TracelessStyleInstance isn’t just metadata — it’s a fully working instance, with create, merge, and cx already bound to your new variants, plus a variants map and an errors array for anything that failed validation.

Example 1 — Simple custom variants

import { tl } from "traceless-style";

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

// Use anywhere:
const $ = tl.create({
  card: {
    padding: "1rem",
    _tablet: { padding: "2rem" },
    _brand:  { color: "gold" },
  },
});

Once _tablet and _brand are registered, they behave exactly like any built-in variant key (_hover, _focus, etc.) inside any tl.create call, in any file, for the rest of the project.

Example 2 — Using the returned instance

const $$ = tl.extend({ variants: { _tablet: "@media (min-width: 900px)" } });

// Both global and returned forms work — variants are registered in one place.
$$.create({ card: { _tablet: { padding: "2rem" } } });
tl.create({ card: { _tablet: { padding: "2rem" } } });    // also works

This matters for larger codebases: you’re never forced to choose between “import the global tl” and “use a scoped instance.” Registration happens once, globally, and both access patterns stay in sync.

Example 3 — Validation errors

const result = tl.extend({
  variants: {
    "1bad": "...",       // invalid identifier
    foo:    "color: red; }", // CSS-injection
  },
});
console.log(result.errors);
// [
//   { key: "1bad", message: "Variant key must be a valid identifier" },
//   { key: "foo",  message: "Invalid selector: contains '}' or ';'" },
// ]

Instead of silently swallowing a bad key or letting a malformed selector leak raw CSS-injection characters into the generated stylesheet, tl.extend returns a structured errors array you can inspect programmatically. The docs also note these same errors are surfaced as console.warn at runtime, so you get a heads-up even if you never check .errors directly.

Selector forms

Form Example Compiled to
Pseudo-class _xyz: ":xyz" .tl:xyz { … }
Pseudo-element _xyz: "::xyz" .tl::xyz { … }
&-anchored ancestor _xyz: ".parent &" .parent .tl { … }
&-anchored sibling _xyz: ".peer ~ &" .peer ~ .tl { … }
Media query _xyz: "@media …" @media … { .tl { … } }
Container query _xyz: "@container …" @container … { .tl { … } }
Supports query _xyz: "@supports …" @supports … { .tl { … } }

Two rules govern the & token: if your selector is a multi-step selector that uses & (like .parent &), the & gets replaced by the unique generated class. If it’s a parent-style selector with no & at all (like :is(.dark *)), it’s used exactly as written.

Validation rules

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

  • The selector must be a non-empty string.
  • The variant key must be a valid JS identifier (or quoted with ").
  • The selector cannot contain raw ;, }, or other CSS-injection characters.
  • @media / @container / @supports rules are explicitly recognized as at-rules.

Why this fits the bigger traceless-style picture

tl.extend‘s “no central config” design isn’t a one-off trick — it’s the same philosophy running through the entire library. traceless-style is a zero-runtime atomic CSS toolkit for React, Next.js, Vite, Remix, Astro, SvelteKit, Qwik, and Solid, built around build-time extraction instead of a runtime CSS-in-JS engine — no Tailwind dependency, no Babel plugin required.

npm install traceless-style
npx traceless-style init

init detects your framework, wires the bundler plugin, and generates your CSS entry — the same two-pass compiler that scans for tl.extend calls is also what powers:

  • WCAG 2.1 + 2.2 contrast validation on every build — AA 4.5:1 / AAA 7:1 / UI 3:1 / focus 3:1 enforced before CSS ever hits disk, with an APCA Lc readout in every diagnostic and an interactive --fix-contrast prompt.
  • Auto dark mode — every color you write gets a derived dark variant via , with pair-aware contrast preservation, overridable per-block via _dark.
  • Auto RTL — physical properties like marginLeft rewrite to logical equivalents like marginInlineStart at build time, so one stylesheet serves every script direction.
  • Strict-by-default lint — inline styles, string classNames, CSS Modules, and Tailwind utilities are blocked outright, backed by a property allowlist and value-injection guards.
  • Diagnostic codes — every error and warning carries a stable TLS#### identifier you can grep and link to docs.
  • Two parsers — a zero-native-deps scanner for smaller projects, and an SWC-backed AST extractor that’s nearly 2× faster on 500-file codebases, with auto picking the right one for you.

How it stacks up against the usual suspects, straight from the project’s own comparison:

Tailwind styled-components CSS Modules traceless-style
Zero runtime
Type-safe styles partial partial
Atomic dedup
WCAG contrast at build time
Auto dark mode manual manual
Auto RTL (logical properties) manual manual
No bundler plugin to install (zero-config init)

There’s also tooling around all of this: a VS Code extension (autocomplete across 280+ properties, inline color swatches, hover docs, quick-fix diagnostics) and a DevTools browser extension for live class inspection and cascade conflict warnings.

Wrap-up

tl.extend is a small API with an outsized consequence: it lets a design system’s vocabulary grow the same way the codebase does — organically, file by file — instead of forcing every new breakpoint or brand selector through a single config bottleneck. Combined with the validation layer catching bad identifiers and injection attempts before they reach your stylesheet, it’s a genuinely safe way to extend an atomic CSS system at scale.

Full docs: https://github.com/sparkgoldentech/traceless-style

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

The running list: major tech layoffs in 2026 where employers cited AI

Related Posts