Skip to main contentSkip to navigation

The three weights a Lux surface sets, and the ramp they are set on.

Lux is set on a variable face and draws three weights: the ordinary one for everything that is read, a half step up for the smallest rung, where a face at the ordinary weight goes thin, and a firmer one for a string that is pressed or names a value. There is no fourth above them.

Loading...

Weight is not keyed to size

The engine keys a default weight to each size rung, and that is the right shape for a default — but it can only ever be a default. The same 14px rung is set at the ordinary weight where it is reading and at the firm one where it is a control, and the same is true at 16, so the rung cannot decide. A string that wants emphasis says so where it stands.

<SizableText size="$2" fontWeight={weight.plain}>Fee tier</SizableText>
<SizableText size="$2" fontWeight={weight.mark}>0.30%</SizableText>

Which means size says rank and weight says pressable-or-named. A heading takes its rank from its size and is set at the same weight as the body under it; what gets marked is a control, a tab, a token pill, a value with a name.

Write size before fontWeight, always. size is a variant, and the engine expands it into a whole style block — size, leading, letterspacing and a weight off its own ladder — at the position the prop sits in. A fontWeight written above it is overwritten by that expansion and vanishes: no warning, no class in the output, a line that compiles and does nothing.

Why these are tokens and not numbers

font-weight already understands 400 and 600, so a ladder of numbers would need no names. These are not those numbers. Zen is variable across 100–900 and the three weights Lux draws are 497, 500 and 606 — between the static cuts, which is where a variable face is worth having. Written as tokens they move with the tenant and with the appearance panel; written as numbers they would be a second opinion in every file that has one.

The type the values carry is the text primitive's own, narrowed to its string members. The primitive's type enumerates the nine cuts and has no room for a custom property; CSSProperties accepts a string but also numbers. The intersection is the one type both take, so a component and a plain style object can each be handed the same value with no cast.

The ramp underneath

A weight is only legible at a size, so the same file publishes the ramp. The design layer builds every rung from one anchor and one spread:

--text-base = 0.875rem * --type-scale
--text-N    = (0.875rem ± Δ * --type-ratio) * --type-scale

Three numbers put the Lux ramp on those two knobs. 8/7 lifts the anchor from 14 to 16, 7/4 is the spread that carries the rung below it to 14 and the one above to 18, and --type-root is the fraction Lux draws every rem rung at. What renders is 11.544 · 13.468 · 15.392 · 17.316 · 21.164, which is measured on the Lux surfaces rather than chosen.

Four rungs the anchor and the spread cannot reach state their own number — the ramp steps by twos where the ratio steps by a ratio, and they agree at three rungs and nowhere else. Each of those four still rides --type-scale and carries the design layer's floor and ceiling, because a rung written flat is a rung a person who needs larger type cannot move.

Installation

npm install @luxfi/ui @hanzo/gui @hanzo/ui @hanzo/design @tanstack/react-query
import '@hanzo/design/styles.css'
import '@luxfi/ui/tokens.css'
import { weight } from '@luxfi/ui/material'

Order matters. Both sheets declare at :root, so the later one wins, and @luxfi/ui/tokens.css is what writes Lux's values into the design layer's knobs. @hanzo/appearance writes those same names inline on <html>, which outranks any sheet, so a knob a person has moved keeps its value.

The ladder

NameTokenValueWhat takes it
weight.plain--weight-normal497Running text, and headings
weight.label--weight-medium500The smallest rung
weight.mark--weight-semibold606A control, a tab, a value with a name

--weight-bold stands with --weight-semibold. Both names are the design layer's and both resolve to 606, because nothing on this surface draws a fourth weight; the ladder publishes three roles, which is the honest count.

What it does not do

The ladder sets what a string is set in and says nothing about how dark it looks. Two variable faces at one numeric weight are not necessarily equally dark — apparent weight is a property of the drawing — so a change here is decided by looking at a rendering, not by arithmetic on the numbers.

Nothing here reaches a string the engine styles through a font scope. Those take --f-weight-N, one rung per size rung, which @luxfi/ui/tokens.css points at these same three values; a value on :root alone cannot reach past a scoped declaration, which is why naming a weight is not by itself enough to change one.