uxskill
Star on GitHub

v3.0 — THE BRAIN · 2026-05-28

The 7 axes of a design system,
and how a brief maps to all of them.

v3.0 introduces a 7-axis synthesizer. Every brief becomes seven continuous values. Those values then compile to palette, type ladder, spacing scale, radius scale, and motion timing. Here's what each axis measures, where it comes from, and how it lands as code.

The full axis table

Axis0.0 end1.0 endCompiles to
warmthcoldwarmRGB warmth-shift
contrastflatdramaticType scale ratio + weight curve
densityairydenseSpacing base + line-height
geometrysharpsoftRadius scale
formalityplayfulcorporateTracking + motion dampening
motionstillkineticAnimation timing + curve
type_personalitygeometrichumanistDisplay family bias

Where the axis values come from

Each axis is derived through three steps:

1. Industry seed

17 documented industries each have a 7-axis seed dictionary. Unknown industries default to neutral 0.5 across the board.

INDUSTRY_SEEDS = {
    "fintech-payments": {"warmth": 0.35, "contrast": 0.55, "density": 0.6,
                         "geometry": 0.4, "formality": 0.75, "motion": 0.45,
                         "type_personality": 0.4},
    "luxury":           {"warmth": 0.55, "contrast": 0.7, "density": 0.3,
                         "geometry": 0.55, "formality": 0.75, "motion": 0.4,
                         "type_personality": 0.75},
    "gaming":           {"warmth": 0.5,  "contrast": 0.85, "density": 0.7,
                         "geometry": 0.35, "formality": 0.3, "motion": 0.85,
                         "type_personality": 0.45},
    # ... 14 more
}

2. Tone nudges

35+ documented tone tags push axes by ±0.10 to ±0.30:

TONE_NUDGES = {
    "warm":         {"warmth": +0.20},
    "bold":         {"contrast": +0.25, "motion": +0.10},
    "minimal":      {"density": -0.30, "contrast": -0.10},
    "rounded":      {"geometry": +0.25, "warmth": +0.10},
    "corporate":    {"formality": +0.25, "warmth": -0.10},
    "kinetic":      {"motion": +0.25, "contrast": +0.10},
    "editorial":    {"type_personality": +0.25, "density": +0.10},
    # ... 28 more
}

3. Forbidden clamps

The brief's forbidden list applies hard clamps to specific axes:

FORBIDDEN_CLAMPS = {
    "playful":    ("formality", (0.6, 1.0)),
    "loud":       ("contrast",  (0.0, 0.5)),
    "dense":      ("density",   (0.0, 0.55)),
    "brutalism":  ("geometry",  (0.3, 1.0)),
}

So forbidden=["playful"] clamps formality to at minimum 0.6, regardless of what the seed + nudges produced.

How each axis lands as code

warmth → palette RGB shift

The palette synthesizer mixes anchor colors from the chosen brand exemplars, then applies a warmth shift based on the axis value. Distance from 0.5 sets intensity, max ±18 RGB units.

def _warmth_shift(rgb, warmth):
    delta = (warmth - 0.5) * 2     # -1.0 .. +1.0
    shift = 18 * delta             # ±18 max
    r, g, b = rgb
    return (r + shift, g + shift / 2, b - shift / 2)

So a brief with warmth=0.7 produces palette canvas ~12 R units warmer, ~6 G warmer, ~6 B cooler than the same brief at warmth=0.5.

contrast → modular scale ratio + weight curve

Pick the type-size ratio from contrast:

So a "minimal/quiet" brief produces tightly-stepped headings (16/19.2/23/27.7/33.2). A "bold/dramatic" brief produces dramatic jumps (16/21.3/28.4/37.9/50.5).

density → spacing base

This is where the axis interaction matrix kicks in (we documented density × formality conflict resolution in the launch post):

densityformalitybase px
> 0.65any4 (Bloomberg)
< 0.4> 0.712 (luxury)
< 0.4< 0.78 (airy SaaS)
elseany6 (mid)

geometry → radius scale

Sharp + corporate = 2px (NYT). Soft + playful = 18px (Glossier). Otherwise blended target.

formality → tracking + motion dampening

High formality tightens display letter-spacing (-0.025em vs -0.015em). Also dampens kinetic motion timings by 25% when motion is also high (the "Bloomberg with hover animations" case).

motion → animation timing + curve pick

base_ms = 220 - 80 * motion, so a kinetic brief gets 140ms base timings, a still brief gets 220ms.

Curve picks:

type_personality → display family bias

Low (geometric) biases the synthesizer toward Inter Tight / Söhne / Grotesque families. High (humanist) biases toward Bricolage Grotesque / IBM Plex / Source families. Body always anchors on Inter.

What this means in practice

You ask for: industry=fintech-payments, tone=["bold", "serious"].

  1. Industry seed: warmth=0.35, contrast=0.55, density=0.6, geometry=0.4, formality=0.75, motion=0.45, type_personality=0.4
  2. Apply "bold" nudge: contrast=0.55+0.25=0.80, motion=0.45+0.10=0.55
  3. Apply "serious" nudge: formality=0.75+0.20=0.95
  4. Final axes: (0.35, 0.80, 0.6, 0.4, 0.95, 0.55, 0.4)

The synthesizer compiles this to:

All deterministic. All from a brief with 3 fields. All offline, no LLM call.

Try it yourself

pip install uxskill
uxskill synthesize \
  --industry fintech-payments \
  --tone bold --tone serious

# Outputs: { mode: "pure_synthesis",
#            axes: {warmth: 0.35, contrast: 0.80, ...},
#            palette: {canvas: "#0a1014", ink: "#f6f8fa", ...},
#            type_pair: {display: "Inter Tight", body: "Inter", ...},
#            spacing: {base: 4, scale: [4, 8, 12, 20, 32, 52, 84]},
#            radius: {base_px: 4, sm: 2, md: 4, lg: 6, xl: 10, pill: 999},
#            motion: {base_ms: 245, fast_ms: 147, slow_ms: 441, ... } }

Related