Skip to content
Recursica

Architecture

Recursica is three JSON files and one rule: every value is defined once, and everything else points at it. The files are plain W3C Design Tokens Community Group (DTCG) documents, so they are portable to any tool that speaks the format, and they export to CSS custom properties for code.

The three files

A Theme Forge export always contains the same three documents, each one a layer of abstraction over the one before it.

1. recursica_tokens.json — the primitives

Raw, unopinionated values with no meaning attached: color scales, sizes, font families, and opacities. A primitive knows it is #e9e9e9; it does not know it is a border.

tokens/colors/scale-01/100   → #e9e9e9
tokens/sizes/0-5x            → 4px

Nothing in the system uses a primitive directly. They exist so that the values below have a single source.

2. recursica_brand.json — the semantics

Meaning, expressed as aliases to primitives. This is where a value becomes a decision: what is interactive, what is text, what a heading is, how deep a layer sits. It holds your light and dark themes, the four layers, the type scale, dimensions, and fonts.

brand/themes/light/layers/layer-0/elements/text/warning
  → {tokens.colors.scale-04.700}

brand/dimensions/icons/sm
  → {tokens.sizes.2x}

Both themes live in this one file, which is why light and dark stay structurally identical: the same semantic names resolve to different primitives.

3. recursica_ui-kit.json — the components

Every component’s every property, aliased to brand semantics. Fifty-five components, each with its variants, sizes, and states spelled out.

ui-kit/components/button/variants/styles/solid/properties/elevation
  → {brand.elevations.elevation-0}

A component never names a color or a pixel value. It names a brand decision, which names a primitive.

Why the chain runs one way

ui-kit  →  brand  →  tokens

Aliases only ever point down the chain, never up and never sideways. That single constraint is what makes the system work:

  • Re-theming is a swap, not a rewrite. Change a primitive and every semantic and every component that resolves through it follows, with no component ever being told a theme changed.
  • There is exactly one place to look. A wrong button color is either a bad component alias, a bad semantic alias, or a bad primitive, and the chain tells you which.
  • Meaning survives the redesign. “Interactive” stays interactive when the brand color changes.

Why DTCG

The format is not a detail; it is the reason any of this is portable.

  • It is a W3C community standard, not a vendor file. Any tool that reads DTCG can read a Recursica theme.
  • Values are typed. $type: color, $type: dimension — a consumer knows what a value means without guessing from its name.
  • Aliases are first-class. The {group.path.name} syntax is part of the spec, so the reference chain above survives export and import intact.
  • It is diffable. Themes live in your repository as JSON that reviews like code.

What comes out

Theme Forge exports the three JSON documents alongside a CSS file of custom properties, one per token, named for its path:

--recursica_tokens_colors_scale-01_100
--recursica_brand_layer_0_elements_interactive_color
--recursica_ui-kit_components_button_variants_content_label_variants_sizes_default_properties_border-radius

The component adapters read only these custom properties, which is why a new export restyles an entire application without touching a line of component code.

Scoped and unscoped variables

The CSS export is not one flat list. It is four kinds of block, and which block a variable lives in tells you what it depends on.

Unscoped: :root

Everything that cannot change with theme or depth: the primitives, plus the theme-independent brand values (dimensions, radii, the type scale, font families). These are global to the document and always resolve.

:root {
  --recursica_tokens_colors_scale-01_100: #e9e9e9;
  --recursica_brand_dimensions_border-radii_xl: 28px;
}

Theme-scoped: [data-recursica-theme="light|dark"]

Everything that depends on light or dark. Set the attribute once on <html> and the whole document resolves. Layer 0 applies by default, so a page that never sets a layer is already correct.

Layer-scoped: [data-recursica-theme="…"] [data-recursica-layer="N"]

Everything that depends on depth. Set data-recursica-layer on a wrapper and its descendants inherit that layer’s values. Each layer block redefines not just the layer’s own surface and text, but the entire component set for that depth, so the same button resolves differently inside a modal than on the page canvas. See Layers for what each depth means.

Generic names versus specific names

Two names exist for most values, and the distinction is the whole mechanism:

  • Generic--recursica_brand_layer_1_properties_surface. No theme or layer in the name. This is what your components use. The cascade fills in the right value based on ancestors.
  • Specific--recursica_brand_themes_light_layers_layer-1_properties_surface. Theme and layer baked into the name. These exist so the generic names have something to point at. Referencing one in a component hardcodes a theme and defeats the system.

The rule follows from that: never match on data-recursica-theme or data-recursica-layer in your own selectors. Set the attributes, use generic names, and let the cascade resolve.

One consequence worth knowing: generic layer 1 through 3 names are only defined inside their layer blocks. An element that needs layer-1 tokens must sit under an ancestor carrying data-recursica-layer="1", or the variable resolves to nothing. Layer 0’s generics resolve at the theme level, which is why the default just works.