Browse all of Kata

Kata migration — bringing every screen onto the new look

Source docs/design/kata-migration.mdMarkdown

On this page

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 testID survives, on the same element. If a bespoke control is replaced by a primitive, its testID is passed through.
  • Accessibility. accessibilityLabel, accessibilityRole, accessibilityState and accessibilityHint survive. 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, grep e2e/ 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. hitSlop stays.
  • 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, label colors.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.red text (never brand.red as 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.errorText with 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.sheet on 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.metric or typography.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.control on a control edge, never border.default or border.input.
  • Colour: no text.quaternary as 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.