Status: in progress (2026-10-01) · Brief: the Kata interview · Brand core: galenjauss/Konjo#179
The brand core changed the tokens and the shared primitives. Every screen that builds its own button, chip, band or label still shows the old look. This document is the one recipe for moving a screen onto Kata. Every person or agent doing the migration follows it, so the result is one design rather than many interpretations.
The prime directive: change how it looks, never what it does. A migrated screen has the same destinations, the same data, the same order of steps, the same tap targets and the same accessibility as before.
What must not change
These are hard rules. A diff that breaks one is wrong, however good it looks.
- Behaviour. No change to an
onPress, a navigation call, a mutation, a query, a conditional that decides whether something renders, or the order of a flow. - Selectors. Every
testIDsurvives, on the same element. If a bespoke control is replaced by a primitive, itstestIDis passed through. - Accessibility.
accessibilityLabel,accessibilityRole,accessibilityStateandaccessibilityHintsurvive. A replacement primitive must expose the same role and label. - Copy. Words stay the same. The one exception is capitals typed into a string: those become sentence
case with
textTransform: 'uppercase'on the style, so the screen looks identical and VoiceOver stops spelling it out. Before changing any visible string, grepe2e/and.maestro/for it. If a test matches it, update the test's matcher in the same change (case-insensitive, or the new string), and say so in your report. - Touch targets. Nothing that was ≥44pt gets smaller.
hitSlopstays. - Layout that holds data. Don't remove a field, collapse a section or hide content to make a screen calmer. Calm comes from type, colour and space, not from deleting things.
The recipes
1. Selected and active states → red
Anything that expresses "this one is chosen" fills colors.brand.red with label/icon
colors.brand.onRed. This applies to chips, filter pills, tabs inside a screen, segmented buttons,
toggled day pickers and choice cards. It does not apply to the hero block or the Studio rail.
Unselected stays as it was: a surface.card fill with a 1px border.control edge.
chipActive: { backgroundColor: colors.brand.red, borderColor: colors.brand.red },
chipActiveText: { color: colors.brand.onRed },
2. Primary buttons → red pill
A filled call-to-action uses <Button> from src/components/ui when the swap is clean: same label, same
handler, same testID, same loading behaviour. If the bespoke button has things <Button> cannot express
(an icon-only control, a two-line label, a timer), keep it bespoke and restyle it:
primaryButton: { backgroundColor: colors.brand.red, borderRadius: radius.pill, minHeight: controlHeight.primary },
primaryButtonText: { color: colors.brand.onRed },
- Secondary buttons are an ink outline pill: no fill,
borderWidth: 1,borderColor: colors.border.control,borderRadius: radius.pill, labelcolors.text.primary. - Destructive buttons are never red. They use the secondary style with a plain verb ("Delete event"). If a destructive button is currently red, make it the outline.
- Links are
colors.accent.redtext (neverbrand.redas text).
3. Red only means "tap here" or "selected"
Red on a label, eyebrow, section header, count, caption or decorative number becomes colors.text.primary
(or text.secondary for metadata). Red stays on:
- links and tappable text;
- live or urgent status ("Starts in 5 min", "Past due"), which is the one non-tappable exception, and only when it is genuinely urgent;
- errors, which use
colors.feedback.errorTextwith an icon and words.
4. The retired diagonal band → Hero, rows, or nothing
DiagonalBand and every band.* token are retired. A band strip becomes one of three things:
- the screen's single
<Hero>block, if it is the screen's one most important thing; - an ordinary row or card, keeping its title, subtitle, arrow and destination;
- nothing, if the same destination already appears elsewhere in the same viewport.
Every destination a band linked to must still be reachable from the same screen.
5. Text reflows, it doesn't clip
numberOfLines={1} clips. Remove it, or raise it to 2, wherever the container can grow. Keep a single line
only where the layout genuinely cannot grow, and say why on the same line with
{/* kata-allow: <reason> */}. Examples where one line is right: a tab-bar label, a chip inside a
horizontal scroller, a segmented-control label, a fixed-size tile. Never truncate a person's name, a class
or event title, or a date that a person needs to read.
6. Shape
- Cards and tiles:
radius.card(16). - Buttons, chips, pills, segmented tracks and avatars:
radius.pill. - Inputs:
radius.input(8). - Sheets:
radius.sheeton the top corners. - No raw radius numbers.
7. Type
Archivo is applied automatically. Never set fontFamily except via a token.
- A screen's large title uses
typography.display. - A hero statement uses
typography.statement. - A big number uses
typography.metricortypography.hero.
Those four carry the condensed display cut. Body, rows and labels stay on the text tokens.
8. The mechanical rules
Fix every design-check finding in the files you own:
- Spacing: move spacing onto the ramp
0 2 4 6 8 12 16 20 24 32 40 48 64 80, nearest value, and prefer the token (spacing.s12). - Borders:
border.controlon a control edge, neverborder.defaultorborder.input. - Colour: no
text.quaternaryas a text colour, and no hardcoded hex (use the theme). - Type size: nothing under 11pt (tab labels excepted).
- Depth: no shadows or elevation. A floating surface uses a hairline and a scrim.
- Caps: no capitals typed into strings.
How to check your work
npx tsc --noEmit # must stay clean
node scripts/verify/design-check.mjs <your folders> # drive it to zero, or to stated kata-allow lines
grep -rn "<any string you changed>" e2e .maestro # keep the tests matching
Then re-read your diff with one question per change: does this alter what happens when someone taps, or only how it looks? If you cannot answer "only how it looks", revert that change.
The surfaces pass (wave 2)
surface.white is deprecated. In dark mode it resolves to the page colour, so a card painted with it
separates from nothing. As a light-mode screen root it puts white cards on a white page, which is the
opposite of the separation law. design-check flags it as kata-surface-white. Replace every use by
what the element is:
| The element is… | Use | And check |
|---|---|---|
| A screen root, safe area, scroll container or list background | colors.surface.page |
Text sitting directly on it: text.tertiary fails there (4.23:1), so make it text.secondary. A surface.tag or surface.muted chip or field directly on it vanishes (≈1.02:1), so make it surface.card with a 1px border.control edge |
| A card, tile, row group, sheet body or modal | colors.surface.card |
Drop a border that only existed to separate white-on-white; keep border.control on anything tappable |
| An input, search field or select | colors.surface.card + 1px border.control |
Inputs sit on the page, not inside a card |
| A full-screen media viewer or camera surface | leave dark surfaces dark: brand.ink / surface.hero as they were |
— |
| Something that must be white in both themes (a QR code field, a printed document preview) | the theme-invariant token that already exists (brand.onRed is #FFFFFF in both) |
Say why in a comment |
A screen that was a white page with bare rows on it becomes a tinted page with those rows grouped into a white card. That is a change of container, not of content: same rows, same order, same handlers.
Look at both themes: surface.page is #F2F2F3 in light and #0E0E10 in dark, and surface.card is
#FFFFFF in light and #1C1C22 in dark.