BeltDot draws a rank as a small ringed circle. RankPicker lists a dojo's own ladder as rows
to choose from; RankPickerSheet puts that list in a sheet.
Light
ranked · 10
roster · 16
subject · 28
white · ring on
black · ring on
shodan
godan
hachidan
kudan
Dark
ranked · 10
roster · 16
subject · 28
white · ring on
black · ring on
shodan
godan
hachidan
kudan
Source:
BeltDot.tsx,RankPicker.tsx. The law behind both: rank. The swatches above use the launch community's ladder as example data; a screen renders whatever ladder the dojo has.
When to use it
BeltDotbeside a rank's name, or beside a person whose rank is also written nearby.RankPickerinline in a form,RankPickerSheetfrom a select trigger.
Never hardcode a ladder, a rank name or a rank colour into a screen. BeltRankPicker does, which
is why it is retired: it can only draw nineteen Cuong Nhu ranks.
Anatomy
BeltDot
| Part | Token |
|---|---|
| Size | swatchSize.ranked 10 (has a rank; legal only where the rank is also text), swatchSize.roster 16 (the rank matters: rosters, pickers), swatchSize.subject 28 (the rank is the subject) |
| Fill | the ladder's own visual: solid, stripes, split, end-cap or alternating bands — the dojo's data, not Konjo's palette |
| Ring | 1px border.control, every rank, both themes, no exceptions |
RankPicker
Light
Selected row carries a check · unavailable rows state why
Dark
Selected row carries a check · unavailable rows state why
| Part | Token |
|---|---|
| Row | controlHeight.row 64 floor, spacing.s12 padding and gap |
| Swatch | BeltDot at swatchSize.roster 16 |
| Name | typography.rowTitle, text.primary |
| Selected | a check at 16 in text.primary on the right |
| Unavailable | full contrast, with its reason in caption text.secondary on the right |
| Divider | 1px border.hairline on surface.card, inset to the text edge |
States
Light
Loading · skeleton rows
Error · bordered retry
Couldn’t load the ranks
The connection dropped. Your choices are kept.
Try againDark
Loading · skeleton rows
Error · bordered retry
Couldn’t load the ranks
The connection dropped. Your choices are kept.
Try again- Selected: a check, never a filled row.
- Unavailable: not dimmed. There is no legal disabled text colour, so the row stays readable and says why ("Needs 6 more months").
- Loading: four skeleton rows of the same shape, never a centred spinner.
- Error: "Couldn't load the ranks", plain words, and a bordered "Try again" — the screen owns the primary.
- Empty: "No ranks set up", and where an owner fixes it.
- Pressed:
opacity.pressed0.85 on the row.
Content
- The rank's name is always beside the swatch. Two ranks can draw identically — nidan and rokudan are both two red stripes on black.
- Shared components say "rank", not "kyu" or "dan": those are one lineage's words.
Accessibility
BeltDotis hidden from screen readers. The rank reaches them through the row's label; announcing it twice is worse than not at all.- The ring is not decoration. Belt fills get no contrast guarantee, and against a card a white
belt, a yellow belt and, in dark, a black belt are close to invisible.
border.controlonsurface.cardis 3.53:1 in light and 3.16:1 in dark, whatever colour it surrounds. - Each picker row is one stop, role
button, withaccessibilityState={{ selected, disabled }}and a label that includes the reason when it is unavailable.
Do and don't
Do
The swatch sits beside the rank’s name.
Don’t
Swatches alone. Nidan and rokudan draw the same.
Code
import { BeltDot, RankPicker } from '../../components/ui';
import { swatchSize } from '../../theme/tokens';
<BeltDot visual={rank.visual} size={swatchSize.roster} />
<RankPicker
ranks={ladder}
selectedSlug={chosen?.slug}
onSelect={setChosen}
unavailable={{ brown: 'Needs 6 more months' }}
loading={ladderLoading}
error={ladderError}
onRetry={reloadLadder}
testID="promotion-rank-picker"
/>