Browse all of Kata

Designing for Konjo

Source .claude/skills/design/SKILL.mdMarkdown

On this page

name: design description: Konjo's design language, in operational form — the tokens, screen formulas, and hard laws that make a screen look and feel like Konjo on the first try. Read it BEFORE writing any user-visible UI in src/ or apps/studio/, and again before claiming a screen is done. Covers layout, spacing, type, colour, empty/loading/error states, progress, and copy voice. Not for backend, migration, or infrastructure work.

Designing for Konjo

This skill bundles references/. SKILL.md carries the laws, the budgets and the formulas; §0 routes to the chapter for whatever you are actually building, and the generated references carry every value and every component that exists. The reasoning behind all of it lives in docs/design/konjo-design-language.md.

Kata (2026-09-29) supersedes parts of this skill — the brief is the Kata interview. Red is the action colour (not a one-per-screen role); the budgets are guidance the reviewer judges; type is Archivo, with Archivo ExtraCondensed for display; cards are 16 with pill buttons; the ninja is the mark and may appear in empty states, errors and celebrations. Where this file still says otherwise, the interview wins.

Loudness is a budget, and each screen gets one purchase. Konjo is athletic and dojo together: Nike-grade energy at the big moments, dojo calm in daily use. The failure mode of loud is that everything shouts and nothing does — which is exactly what the current app gets wrong. Spend the volume on one element per screen and make everything else quiet enough to disappear.

read the screen's job → pick the formula → build with tokens only
   → check the three budgets → walk it as the persona who'd quit
   → fix what you find → then say it's done
        ↑____________ iterate ____________|

0. Where to go for more

Read the pattern chapter for the shape you are building — always, before you start. This file carries the laws, the budgets and the sketches; the pattern chapter carries the decisions that only make sense for that shape, and it is short.

Then load a foundation chapter for anything on your screen you do not have a rule for. A real screen usually needs two or three: a student detail is a detail + rank + content-format, a booking flow is a form + sheet-picker + destructive. That is normal and it is what the router is for — do not ration yourself down to one and then invent the rest. Inventing is the failure this system exists to prevent.

What you should not do is read the whole design language front to back. Take the rows you need.

Building Read
A tab home tab-home
A list or index index-list
A grid of people tile-grid
A detail screen detail
Deciding push vs modal, or a header navigation
Anything typed into form · sheet-picker
Something irreversible destructive
Anything dragged, dropped, or reordered direct-manipulation — and its keyboard path is not optional
Columns of cards, one per stage board — the container; the gesture is the row above
A search field or filter chips search-filter
Any button, anywhere buttons
Empty / loading / error states
Push, a banner, a toast, a badge notifications
Streaks, badges, promotion progress · celebration
Anything in Studio studio/dialect and studio/metrics — the foundations row below is mobile metrics, and building a Studio screen to a 44pt target and a 20pt gutter is the most expensive wrong turn on offer
A Studio dashboard or snapshot cards studio/dashboard
A table studio/tables
A drawer, menu, dropdown or toast in Studio studio/overlays — a floating surface's boundary is border.control, not the hairline
A chart studio/charts — and load the dataviz skill
Which Studio screen this is, and what is on it studio/ia-and-inventory
Studio's navigation rail studio/metrics
Needing a value or a rule Read
Any token, any ratio references/tokens.md — generated, always current
What already exists references/components.md — generated from the barrel
Colour · type · space · borders · metrics foundations/ — mobile. In Studio these are overridden by studio/metrics
Anything that animates motion
An icon iconography
A photo, avatar, or the mascot imagery
A belt or rank rank
A date, number, name, or count content-format
Screen readers, text scaling, focus accessibility
The wordmark, icon, splash, or store art brand
Anything permanent — who's alerted, what's undoable product-facts — read, don't ask
Adding or changing a token governance

1. Before you write a line

Answer these. If you can't, you're not ready to design — go back to the ship interview.

  • Whose screen is this? The white belt in week two (quits when made to feel stupid), the student logging one-handed and sweaty with 40 seconds, the instructor mid-class with one free hand, or the owner doing admin they resent.
  • What is the one thing they came for? That is the loud thing. Everything else is quiet.
  • What does it look like with no data? With bad data? For someone without access?
  • What does this actually do? For anything that writes a permanent record — a promotion, a waiver, a payment, a removal — you cannot write the screen without knowing what becomes permanent, who is alerted, who can read it afterwards, and whether it can be undone.

Check product-facts.md first — it is the answer, not a question. Promotions, incidents, removals, waivers and push categories are decided there.

If it is not there, that is a blocker, not a blank to fill. A consequence line invented to satisfy a rule is worse than no consequence line: it is a confident false statement on the one screen where people are trusting the text. Say which fact you are missing and stop. Everything else on the screen can be built while you wait — but never guess what an irreversible action does, and never soften the gap with vaguer copy.

2. The three budgets — guidance, judged per screen

Budget Allowance Counted as
Loud block (hero, inverted surface, display type) 1, or none Instances
Filled primary button 1 — everything else is a text link or a bordered control Instances
Uppercase label register 1 — labels only, ≤3 words, ≤20 chars, never a sentence Type tokens

Kata: guidance, not a hard count. The reviewer asks whether the screen has one clear focus. Going over needs a reason you can state; it is not an automatic fail.

These are ceilings, not quotas. A screen with zero loud blocks and zero filled primaries is legal and common — an index screen usually looks exactly like that.

Chrome is exempt — tab bar, status bar, header icons, and the display screen title. A title is not the screen's loud block, which is why an index screen ships one and still spends zero. Control states are exempt too — a selected chip or a checked-in tile filled with surface.hero is state, not a loud block.

Red is not budgeted — it is the action colour. A filled primary, a link, a selected segment, a checked box and the active tab are red. Red that is not tappable and not a selected state is still wrong: no red decoration, no red body text, no red block that does nothing. Destructive actions are never red — an ink outline and a plain verb.

Uppercase counts label tokens. eyebrow is the only uppercase label register in the scale, so the way this budget actually gets broken is not by adding a second token — it is by pressing statement into service as a section label, or by typing capitals by hand. The hero's statement is display voice: a caps eyebrow above a caps statement is legal. A caps statement sitting over a list, doing an eyebrow's job, is not.

3. Pick a formula

First decide whether this tab home has a hero at all. Hero-led when there is usually one obviously-next thing for this person (Train, Teach). Index-led when they came to choose among many (Social, Learn) — section header, then the list, no inverted block. Events is conditional: hero when they're enrolled in something soon, index when they aren't. Applying the hero shape to a browse tab buries the browsing, which is the most likely way to misuse this skill.

Tab home (index-led) — Social, Learn, Events-without-an-enrolment:

Title (display 34/800)                    ◎ ⬤
[ ⌕ Search…            ]  Filters            input h44, border.control; search first
( chip ×) ( chip ×)                          h32, surface.card + border.control
┌ FOR YOU ──────────────────────────────┐    at most ONE personal card above the index
│ ● One Green Stripe                  › │
└───────────────────────────────────────┘
BROWSE                                       eyebrow, text.secondary, on the page
┌───────────────────────────────────────┐
│ Kata                            4   › │    the index = one card of hairline rows
│ ─────────────────────────────────────  │
│ Hand techniques                16   › │
└───────────────────────────────────────┘

No inverted block — the index is the point, and a hero above it is a wall in front of the door. Two cards above the index and you have rebuilt a dashboard. Empty search keeps the field and filters on screen; an empty collection replaces the index.

Tab home (hero-led) — one hero, then a little:

Title (display 34/800)                    ⌕ ◎ ⬤   24pt glyphs, 44pt targets
▓▓ full-bleed surface.hero ▓▓                        pad 20h / 24v, radius 0
▓ eyebrow 11/800 caps · accent.onHero        ▓
▓ statement 46/900 caps · text.onHero        ▓       reflow to 3 lines, then step to display
▓ meta 15/400 · text.onHeroMuted             ▓
▓ [ ONE RED CTA → ] 48pt pill, hugs left     ▓       the block itself is NOT pressable
Quiet row                                        →   rowTitle 16/600 + caption 13 secondary
Quiet row                                        →
░ one tonal tile ░

The hero is a solid block carrying type, never a photograph — it has to work for a dojo that has never uploaded an image. If a sixth section wants on the screen, it goes one tap away.

surface.hero inverts by theme — #111111 light, #F2F2F2 dark. It is the maximally contrasting surface, not "the dark one". Painting it brand.ink makes it 1.021:1 on the dark page and the screen's centrepiece vanishes. Text on it is text.onHero / text.onHeroMuted / accent.onHero — never a hardcoded hex, and never brand.red (3.77:1 on ink).

The hero block is not pressable. Only its CTA navigates, or the same destination appears twice in one viewport.

List row — the thing they're looking for is first and boldest:

Primary line              rowTitle 16/600
Secondary · metadata      caption 13/400 text.secondary       row ≥64pt

Never invert this. Hairlines are inset to the text edge; ≤5 rows get no hairlines at all, just 12pt between groups.

Tile grid — for triage plus one tap per person (attendance, check-in, a queue). 111×92pt tiles, 3 across at 390pt, 8pt gaps. Ink fill = done, white = still outstanding — never red, or you spend the budget once per tile. One bare status glyph top-right (a tinted chip is 1.14:1 on white and disappears); belt dot top-left with a hairline ring, or a white belt is invisible on a white tile. One glyph per tile by priority, with the accessibilityLabel enumerating all of them. A tappable "N need attention" row above the grid is the legend — filtering to those people teaches the glyphs without a key.

Detail — eyebrow → title → meta → primary action → content. The ID or category is an eyebrow above the title, never a chip beside it.

Form — the full spec is patterns/form.md; the load-bearing parts:

SECTION HEADER                  eyebrow · text.secondary
                          ↕12
One-line summary                label 13/700 SENTENCE case
┌────────────────────────┐ ↕6
│                        │      h≥44 · radius.input · 1px border.control · surface.card
└────────────────────────┘      ↕16 to the next field · ↕24 to the next section
Where              Optional     mark what is OPTIONAL, not what is required

Inputs sit on the page, never inside a white card — an input's fill is surface.card, so an input in a card is white-on-white. Group with space and an eyebrow, not a bordered box. Chips wrap, never scroll horizontally.

Validate a typed input on blur, never on keystroke. A select trigger has no blur event — you tap it, a sheet opens, it closes — so a trigger validates on submit, then re-validates live once it has been in an error state. Most Konjo forms are mostly triggers, so this is the common case, not the exception. Error = border swaps to feedback.errorBorder (still 1px) + caption in feedback.errorText 6pt below. Focus = 2px border.focus — the one of three sanctioned uses of borderWidth.emphasis 2, and without it the form is unusable with a keyboard on RN-web, which is the surface CI actually walks.

Never disable submit (there is no legal disabled text colour) — validate on tap and jump to the first problem. In flight: fill stays, label → "Submitting…", 16pt spinner left, inputs editable={false}, button accessibilityState={{ busy: true }}. A failed submit never replaces the form — a bordered block above the button, and the button relabels "Try again". Success pops back to the list the record joins, with a one-line confirmation naming who was affected.

Pickers — chips inline for 2–6 options (the choice stays visible); a sheet with a row list for 7–20; a sheet with a pinned search for 20+; the native picker for dates and times, always. Sheet: grabber, left-aligned title, rows ≥64pt with a check on the selected row (a filled row inside a sheet reads as a hero), radius.sheet on the top corners, separated by the overlay.dim scrim and nothing else. Single-select closes on tap — the tap is the confirmation. A picker never validates; the field it feeds does.

Serious screens (legal records, alerts, money): state the consequence before the tap, never pre-select a field that carries liability, and keep the copy clinical — roles not names, no reassurance, no apology. Destructive actions arm on first tap, fire on second.

Buttons — use <Button>. Primary fills brand.red with brand.onRed label, h48, radius.pill, label rowTitle 16/600. Secondary (and destructive): no fill, 1px border.control pill in ink, h44, label 13/700. Link: accent.red text. label 13/700 is for chips, secondary buttons and links — a 48pt button carrying 13pt type is anti-pattern #13.

Error — the Empty anatomy: eyebrow → "Couldn't load events" → plain words about what failed, no error codes → [ Try again ]. The retry inherits the weight of what it replaced: a failed screen renders nothing else, so its retry is the filled primary; a failed section sits under a screen whose primary is still on screen, so its retry is a bordered secondary. That is how the budget survives a partial failure. Left-aligned, in flow. Field errors sit inline below the field and fire on blur, never on keystroke.

Empty — EmptyState already exists (24 call sites); extend it, don't write another. First ask whether the screen should be empty at all. Real data elsewhere that belongs here? Show it. Can you suggest from a real source? Suggest it. Only if genuinely nothing: eyebrow → title → what goes here and why → one filled primary → one text link, left-aligned and in flow, never centred in a void. Demote the screen's other action while the empty state shows.

Loading — under 1s show nothing. 1–10s a spinner; a skeleton only where the layout is predictable and the user has seen it before. Over 10s, determinate progress.

4. Build with tokens only

Full tables in references/tokens.md. The ones you'll reach for:

Space — 0 2 4 6 8 12 16 20 24 32 40 48 64 80. 8 is the spine; 4/6/12/20 are the legal half-steps (6 for gaps inside a component, not layout); 2 is optical correction only, never a layout gap. Aliases: page 20, section 24, card 16, tight 8. Group with space, not lines.

Three unrelated things are called hero. typography.hero is a 64pt numeral; surface.hero is the inverting surface; Hero is the component that paints one with the other. A screen can use the surface without the type, and usually should.

Type — hero 64/900 (a numeral), statement 46/900 (the hero's words), display 34/800, title 22/700, rowTitle 16/600, bodyLarge 16/400, body 15/400, caption 13/400, micro 12/500, eyebrow 11/800 caps, label 13/700 sentence case. Every entry has a line height — eyebrow included, because it is the only uppercase register and sits above every section on every screen. Fourteen legacy entries still lack one; the token tables name them. Whatever the entry, give a container holding text room to grow and never a fixed height. eyebrow is the only uppercase label register — the one exception is a CTA inside the hero block, which takes the hero's display voice. Two sizes should carry any screen. A hero is bigger than the page title — 2pt larger than its neighbours is not a hero.

Separation — white cards on a tinted page (surface.card #FFFFFF on surface.page #F2F2F3 — dark: #1C1C22 on #0E0E10), the way Apple Health and Attio do it. Not the other way round: a #FAFAFA card on a white page is 1.044:1 and invisible. No shadow, no blur, no glow, no gradient.

Two border tokens, two jobs: border.hairline 1px divides rows inside a list (inset to the text edge); border.control 1px draws the edge of something tappable — chip, secondary button, input. A chip with only a surface.tag fill is 1.017:1 on the page and is not a control anyone can see.

The card-only family — legal on a white card, illegal on the tinted page: text.tertiary (text.tertiary on surface.page is 4.23:1 → use text.secondary) and surface.tag (1.017:1 → use a white chip with border.control).

Everything named here ships. The tokens are in packages/design-tokens/tokens.ts, re-exported by src/theme/tokens.ts so either import path works, and the primitives — Text, Stack, Card, Row, Button, Badge, Hero, EmptyState — are exported from src/components/ui. Reach for the primitive before you compose one by hand. The full list, including what is retired, is in references/components.md.

Colour — brand.red #D62828 is the action fill (white on it, 5.01:1, both themes). accent.red is red as text (links) — #D62828 as text is 4.48:1 on the light page and 3.85:1 on the dark one, and fails. Ink for section labels, eyebrows, arrows and chevrons. text.tertiary is #737373 and is card-only — text.tertiary on surface.page is 4.23:1 and fails, so text sitting directly on the page uses text.secondary. text.quaternary is not a text colour — never put type in it. Red on the hero surface is accent.onHero, never accent.red — brand.red on ink is 3.77:1 and fails AA. The two tokens swap by theme, because the hero inverts and the page does not: accent.red is #C42020 light / #FF5252 dark, accent.onHero is #FF5252 light / #C42020 dark. Name the surface you are painting on and let the token resolve; typing either hex yourself gets one theme wrong.

Radius — input 8, card 16, large 16, sheet 24, pill 999. Nothing else. Buttons and chips are pill.

Type — Archivo for every word; the app applies it automatically (a fontWeight becomes the matching Archivo family — see src/theme/brandFont.ts), so never set a fontFamily by hand. The display tokens (hero, statement, metric, display) carry Archivo ExtraCondensed already. Bebas Neue is the KONJO wordmark and nothing else.

The small metrics — inputs h44, radius.input, border.control, 16pt inline glyph. Icons 16 inline / 24 standalone / 19 status glyph; icon colour text.primary, or text.tertiary for a chevron. Avatars 32 row and header (header gets a 1px hairline ring), 88 profile. Chips 32 tall, 12 pad-h, radius.pill. Scroll inset above the tab bar insets.bottom + 80. Skeleton fill border.input — a placeholder, on surface.card or surface.page alike. Pressed opacity 0.85. Belt dots always carry a 1px border.control ring — without it a white belt is 1.2:1 on a white card and a black belt is 1.01:1 on a dark one.

Motion — instant 100, quick 150, base 200, enter 250, exit 180, slow 350. Exits are faster than entrances. Nothing over 400ms except a belt promotion. Reduce-motion replaces movement with a cross-fade.

Never type a number a token covers. Never import colours except through useTheme() / createThemedStyles. Never use DiagonalBand or band.* — both are retired.

5. Write the copy plain

Say the thing. "Nothing graded yet / Add what you're teaching to start grading." Not "Nothing here yet — let's fix that!" No exclamation marks outside a real celebration, no emoji in chrome, no first person from the app. Use the art's words: dojo, belt, rank, promotion, test, kata, sensei — never gym-generic synonyms. But a shared component says "rank", not "kyu" or "dan": those are one lineage's words, and Konjo is multi-style. Use the art's vocabulary in copy about this dojo; use neutral vocabulary in anything rendered for every dojo.

For progress: Duolingo's mechanics, Konjo's voice. Streaks and badges are the display; belts are the highest tier of badge because a human conferred them. A streak is stated, a rank is celebrated. No mascots in progress UI, no confetti for showing up, and never a loss-aversion nudge — a dojo does not guilt people into attending.

6. Check it before you claim it

Run this list against your own screen. Every item is a real defect found in the shipped app.

  • Contrast computed, not eyeballed: 4.5:1 body, 3:1 for ≥24px or any control boundary.
  • Every tappable thing ≥44×44pt, or has hitSlop making up the difference. In Studio: ≥32pt, and no hitSlop — that is a touch remedy and Studio is pointer input.
  • Nothing under 11pt — with one exemption, tabLabelActive / tabLabelInactive at 10, which matches the platform's own tab bar. No fixed-height row containing text (Dynamic Type reaches ~46pt).
  • The three budgets in §2 each spent at most once, or the reason written in the PR — zero is fine and normal on an index.
  • Red only on things you can tap or on a selected state; nothing destructive is red.
  • Empty, loading, and error states all exist and were looked at.
  • Nothing truncates. If it doesn't fit, it reflows.
  • The same destination does not appear twice in one viewport.
  • Uppercase applied with textTransform, never by typing capitals (VoiceOver reads a literal "ADD" as "A. D. D.").
  • Interactive elements and the screen root have kebab-case testIDs.
  • Verified in both themes. Light is where you design; dark is where you check. Studio is light only — the ink rail is brand, not a dark theme — so there is nothing to check there.

Then look at the reference screens in docs/design/reference/ — open a .jpg with Read and you will actually see it, including the Konjo targets in reference/konjo/ — two tab homes, a tile grid, the promotion certificate, and a form, all in both themes. Compare yours to the closest one. "Roughly similar" is not the bar; find the specific difference in spacing, weight, or ratio and close it.

What never ships

A screen with no empty state. A mutation with no error path. A list with no loading state. Two controls of equal weight competing to be primary. A centred button floating in a void. Truncation used as a layout strategy. Text over a photo or gradient with no scrim. A gradient nav bar. A decorative accent that means nothing. A row whose metadata is larger than its subject. Copy in developer voice. A hardcoded hex. A magic number where a token exists.

More, with the product each was observed in: references/anti-patterns.md.

The bar

Not "it matches the tokens." The bar is that the person you designed it for — the white belt who feels stupid, the instructor with one free hand — gets what they came for on the first screen, without reading a paragraph, and would do it again next week without being told to.

If you cannot point at the one loud thing on your screen within a second of looking at it, it isn't finished.