DNA — the helix-ui theme engine
DNA — Design Nucleotide Allele. It’s how helix-ui names its theme system.
A DNA is a complete theme genome — 23 genes, one allele each. You
can clone DNAs, mutate them, breed two parents into a child, or run
generations of breeding over a population. Dominant alleles win in crossover;
recessive alleles still surface occasionally. That’s where surprise looks
come from across generations.
Live demo: /showcase/dna-lab — every component on the
page renders under a DNA you can mutate live.
Genes (23)
| Gene | Trait | Sample alleles |
|---|---|---|
accent | Brand hue (drives the OKLCH 50–900 ramp) | blue, violet, crimson, system-blue, fluent-blue… |
chroma | Saturation multiplier across the brand ramp | muted, standard, vibrant |
lightness | Light / dark / auto | light, dark, auto |
neutralTilt | Which way the greys lean | true, cool, graphite, warm, brown, steel |
successHue | Hue of the success ramp | emerald, lime, teal, mint |
warningHue | Hue of the warning ramp | amber, orange, yellow, gold |
dangerHue | Hue of the danger ramp | red, crimson, rose, pink |
infoHue | Hue of the info ramp | sky, cyan, blue, indigo |
contrast | Foreground / background separation | subtle, normal, high |
typography | Body font family + size scale | sans, system, grotesk, rounded, serif, mono |
headingFamily | Font for headings, separate from body | sans, system, grotesk, serif, display, mono |
monoFamily | Font for code surfaces | mono, rounded, serif |
typeScale | Heading-to-body ratio | minor, standard, major, fourth, golden |
lineHeight | Body + heading line-height | tight, cozy, loose |
tracking | Letter-spacing, by size band | normal, tight, flat, airy |
radius | Corner-radius scale factor | sharp, crisp, subtle, soft, standard, rounded… |
radiusCurve | Uniform vs. progressive radius scaling | uniform, swell, gentle, progressive, dramatic |
borderWidth | Stroke weights | hairline, thin, normal, thick |
surface | Depth treatment | flat, elevated, glassy, vibrancy, tonal, acrylic |
density | Spacing scale factor | compact, comfortable, spacious |
paddingScale | Component padding + control height | dense, tight, normal, generous |
motion | Animation duration scale | subtle, standard, lively |
easing | Motion curve identity | standard, smooth, snappy, ios, material3, fluent |
Each allele has a dominance score (1–10) and an optional recessive flag.
The full registry lives in @helix-ui/dna/src/alleles.ts.
Genotype and environment
A genome is not the whole picture. The same genotype expressed in a different environment produces a different phenotype, and a design system has exactly one environment that matters: the operating system it is being looked at on.
That is DNA.platform, and it is deliberately not a gene — it is not
inherited, because breeding an iOS theme with a Windows one should not produce
something half-native to each.
type PlatformId = 'auto' | 'web' | 'ios' | 'macos' | 'android' | 'windows';It carries the decisions each platform treats as a rule rather than a preference — the ones no amount of gene-tuning can express:
| Platform | Focus ring | Hit target | Dark surface | Motion |
|---|---|---|---|---|
ios | 3px @ 1 | 44pt | near-black (OLED) | ×1.15 |
android | 3px @ 2 | 48dp | dark, tinted | ×1.10 |
macos | 3px @ 1 | 28px | mid-dark | ×0.90 |
windows | 2px @ 1, double stroke | 32px | mid-dark | ×0.80 |
web | 2px @ 2 | 24px | ramp default | ×1.00 |
'auto' is the default, including on the wildtype: it resolves against the
real device on the client, so a build that does nothing already respects
Android’s 48dp target and iOS’s near-black dark mode. Anything unrecognised —
Linux, a television, a CI browser — lands on web, which is helix-ui’s own
house style and a guaranteed no-op.
import { PRESETS, PLATFORM_PRESET_KEYS, express, platformVars } from '@helix-ui/dna';
PRESETS.ios();PRESETS.macos();PRESETS.android();PRESETS.windows();
// Force a platform onto any genome — what a page showing all four does.express(PRESETS.noir(), { platform: 'windows' });
// No DNA at all? `<ThemeProvider platform>` uses this under the hood.platformVars('macos');There are four presets, and there used to be three. apple covered iOS and
macOS together, and that is the one merge the metrics do not survive: the HIG
asks for a 44pt touch target and AppKit ships a 32px push button. apple,
material and fluent still resolve as aliases.
Each platform preset also adopts its platform’s accent, which is what makes it
read as native at a glance. That is one gene to put back:
mutate(PRESETS.ios(), { gene: 'accent', allele: 'blue' }).
API
import { wildtype, PRESETS, clone, mutate, compose, evolve, express, type DNA,} from '@helix-ui/dna';
const a = wildtype(); // base DNAconst b = PRESETS.botanic(); // a presetconst c = mutate(a, { gene: 'accent', allele: 'crimson' });const child = compose(a, b); // cross two DNAsconst next = evolve([a, b, c, child]); // run a generation
const { cssVars, dataTheme } = express(child); // → CSS custom propertiesApply a DNA to a subtree via <HelixUIDNAProvider> (from @helix-ui/core):
import { HelixUIDNAProvider, Button } from '@helix-ui/core';
<HelixUIDNAProvider dna={child}> <Button>This button renders under the bred DNA.</Button></HelixUIDNAProvider>;Inheritance rules
When two DNAs cross, for each gene:
- Higher dominance wins. Ties are decided by coin flip.
- Recessive surfacing. If the loser allele is flagged
recessive, it takes the locus ~15% of the time anyway. That’s how visual surprises emerge. - Mutation. A small per-gene mutation rate (default ~4%) replaces the winner with a random other allele.
Tune all three per call (compose(a, b, { mutationRate: 0.2 }) or
evolve(pop, { fitness: contrastScore })).
Why DNA changes are instant
helix-ui tokens ship in two layers:
- Primitives (
color.brand.500,space.4,radius.md,font.size.md,shadow.md) — concrete CSS variable values. - Semantic aliases (
color.bg.action.brand.default = {color.brand.500}) — emitted asvar(--helix-ui-color-brand-500)in the built CSS.
When DNA’s express() overrides a primitive (e.g.
--helix-ui-color-brand-500), every semantic that references it follows
through the cascade automatically. No re-render. No recomputation. Just
a style mutation on the DNA provider’s wrapping div.
Phenotype
express(dna) produces:
- Colour ramps generated with
oklch(L% C H)for brand, neutral, success, warning, danger and info — 10 shades each, from the hue genes andchroma. - Every semantic alias that chains through one of those ramps, re-emitted on the wrapper so the cascade resolves against this genome.
- Spacing tokens scaled by
density.spacing. - Radius tokens scaled by
radius.factorand shaped byradiusCurve. - Font families, a size scale derived from
typeScale, line heights andtracking. - Control heights from
paddingScale, clamped to a 24px floor. - Shadow set and material channel per
surface.style. - Motion durations and easing curves.
- A
data-themeattribute whenlightnessislightordark, and aplatformwhen one was resolved.
{ '--helix-ui-color-brand-500': 'oklch(56% 0.16 234)', '--helix-ui-color-brand-600': 'oklch(47% 0.155 234)', '--helix-ui-radius-md': '11.20px', '--helix-ui-space-4': '20.00px', '--helix-ui-font-family-sans': "'Geist', 'Space Grotesk', …", '--helix-ui-font-tracking-heading': '-0.011em', '--helix-ui-control-height-md': '48px', '--helix-ui-shadow-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), …', '--helix-ui-motion-duration-normal': '200ms', /* …~170 vars total, plus 3 more when a platform is named… */}Persistence
A DNA is a plain JSON-serialisable object. To persist a user’s chosen
theme, JSON.stringify(dna) to localStorage / your backend; JSON.parse to
restore. Allele values are inert data — no functions or class instances.