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.
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-scaleThree 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-queryimport '@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
| Name | Token | Value | What takes it |
|---|---|---|---|
weight.plain | --weight-normal | 497 | Running text, and headings |
weight.label | --weight-medium | 500 | The smallest rung |
weight.mark | --weight-semibold | 606 | A 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.