Browse all of Kata

Rank

Source docs/design/foundations/rank.mdMarkdown

On this page

Belts as data. The one foundation that is Konjo's and nobody else's.

Part of the Konjo design language. The laws in the spine apply here unless this chapter contradicts them.

Rank is the most identity-defining element in the product. It is the thing a student checks, the thing an instructor confers, and the thing the certificate exists to celebrate. It is also, until this chapter, the least specified: twenty raw colour constants living outside the theme, two incompatible renderer models, three copies of the data, and eight rank-and-theme combinations that fail contrast outright.

Rank is a ladder, and the ladder belongs to the dojo

Konjo is multi-style by design. Cuong Nhu is the launch community, not the model. The schema already reflects this — styles and style_ranks carry each dojo's ladder, Studio has an authoring surface with an approval flow, and user_style_ranks records what a person holds in each art they practise.

So the design rule is simple and absolute:

Never hardcode a rank ladder, a rank name, or a rank colour into a component. A screen renders whatever ladder the dojo has. Nineteen is a Cuong Nhu number; ten is an ATA number; the next dojo will have neither.

That extends to language. "Kyu" and "dan" are Japanese-lineage terms; a BJJ academy has neither, and a Taekwondo school uses "gup". Shared components say rank; the dojo's own labels come from its ladder rows. Same for honorifics — see content format.

The swatch, and the ring that makes it legible

A rank renders as a small circular swatch: BeltDot on mobile, BeltSwatch in Studio.

Every swatch carries a 1px border.control ring — every rank, both themes, no exceptions.

This is not a nicety. Measured against surface.card, at the 3:1 that SC 1.4.11 requires of a meaningful non-text mark:

Light Dark
Fails white 1.00, yellow 1.65, orange 2.70 black 1.02, purple 1.80, camo 2.06, brown 2.38, blue 2.95
Count 3 of 10 5 of 10

Eight failures, at both ends of every ladder — the white belt who just started and the black belt who has been there twenty years. Conditional rings do not solve this: a rule like "ring it if the fill is white" leaves the black belt invisible in dark mode, which is exactly what shipped. A ring on everything solves all eight at once, because border.control itself clears the threshold against the card in both themes (3.53 light, 3.16 dark) no matter what it surrounds.

The ring width is borderWidth.hairline. Half-pixel borders vanish at dpr 1 and round inconsistently at dpr 3.

Rendering all nineteen ranks at 10 / 16 / 28pt in both themes confirms the arithmetic and shows what it costs to skip: without the ring, in dark, the black belt is three faint smudges and the entire dan progression reads as red stripes floating on nothing, because the black belt behind them has merged into the card. With it, every rank has an edge and the dan belts are belts again.

Size carries the ladder, and 10pt does not

The same render surfaced something the arithmetic could not. BeltDot's default size is 10pt, and at 10pt a nineteen-rank ladder collapses: shodan, nidan, sandan, yondan and rokudan are all a small dark circle with a hint of red, and nothing distinguishes them.

That is not a bug in the component — 10pt is fine for what it usually does, which is signal that a person has a rank next to their name. But it means:

  • 10pt says "ranked", not "which rank". Legal only where the exact rank is also present as text, or is not the point.
  • A rank is never chosen from chips. Pickers prefer chips at six options or fewer, but a chip is 32pt tall and cannot carry a 16pt swatch with its ring and stay a chip. A short ladder still uses the sheet: rows are the only control where the swatch is legible and the rank's name sits beside it.
  • Where the specific rank matters — a roster tile, a profile, a promotion — use 16pt or larger, and 28pt where the rank is the subject.
  • The rank's name is always available as text or in the containing label. The swatch is never the sole carrier at any size; see the screen-reader contract below.

Rank colour is data, not theme

Belt colours are the dojo's, not Konjo's. They do not live in the palette, they do not invert by theme, and they are not subject to the red budget — a red belt is a fact about a person, not an accent.

But that means they get no protection from the theme either, so:

  • The ring is what guarantees legibility, not the colour. See above.
  • Never derive a UI colour from a rank. No belt-tinted row backgrounds, no belt-coloured avatar rings, no gradient seeded from a rank. A representative colour extracted from a ladder row has no contrast contract with anything, and using it as a surface or a text colour is how a screen becomes unreadable for exactly one student.
  • Rank appears once per element. A row with a belt dot does not also carry a belt-coloured border and the rank spelled out in a coloured chip. Dot plus text, or dot alone with the rank in the accessibilityLabel.

Screen-reader contract

A swatch is a coloured circle: it says nothing on its own, and colour is the only channel it uses — which is a failure of SC 1.4.1 if it is the sole carrier of the fact.

  • The rank's label always reaches the screen reader, either as adjacent text or inside the containing row's accessibilityLabel. On the tile grid, where the dot is the only visible rank marker, the tile's label spells it: "Ana Torres, white belt, not checked in, waiver missing."
  • The swatch itself is decorative once its meaning is in the label — accessibilityElementsHidden, so the rank is not announced twice.

Two ranks must never render the same

A ladder where two rows produce an identical swatch has lost information. In the shipped Cuong Nhu ladder, nidan and rokudan are both two red stripes on black and are indistinguishable — a sixth-degree black belt renders exactly like a second-degree.

Rendering distinctness is a property of the ladder, and the authoring surface is where it is enforced. Studio's ladder editor should refuse to save two rows with the same visual, the same way it would refuse two rows with the same name. Downstream, a swatch is never the only thing distinguishing two ranks in a list — the label is always there too.

The visual model

style_ranks.belt_visual is jsonb. Today it carries {color, stripeColor?, stripeCount?}, which expresses solid and striped belts and nothing else.

That is narrower than the Cuong Nhu ladder actually needs. Three of its nineteen ranks — godan's split belt, hachidan's end cap, kudan's alternating bands — cannot be expressed in that shape at all, so a dojo authoring the same ladder through Studio silently gets three ranks that render as plain stripes. Mobile and Studio can therefore show different belts for the same person, which is the sharpest form of the drift this design system exists to prevent.

The target is one model that both surfaces read, expressive enough for the ladders that exist:

Kind Shape
solid one fill
stripes base plus 1–4 evenly spaced bands
split two halves
end-cap base plus a band at one end
alternating N equal bands of two colours

One schema, one renderer contract, one set of colours per ladder row — and the built-in ladders seeded through the same path as a dojo's custom one, rather than living as constants in two apps.

Never

  • A hardcoded ladder in a component.
  • A conditional ring.
  • A UI colour derived from a rank.
  • Rank conveyed by colour alone.
  • Two ladder rows that render identically.
  • A rank name assumed to be Japanese.