Patterns
Reusable solutions to recurring interface problems: when, anatomy, behaviour, accessibility, language and tokens.
A pattern is a reusable solution to a recurring interface problem, written so that a designer or a developer can apply it without reinventing it. Every pattern states when to use it, its anatomy, behaviour and states, its accessibility requirements, what changes across the four languages, and the tokens it uses. Components in the app implement patterns; patterns never reference a component library.
| Pattern | Problem it solves |
|---|---|
| foundations-usage | How tokens are used: roles over primitives, themes, type roles, spacing grid |
| layout | Page structure, containers, grid, responsive behaviour from phone to desktop |
| navigation | Moving between sections on web and mobile; context switching |
| forms | Collecting input: fields, validation, errors, long forms |
| data-display | Lists and tables of people, groups, results; sorting, filtering, pagination |
| actions | Buttons, menus, destructive actions, confirmation |
| feedback | Loading, empty, error and success states; notifications |
| dialogs-and-consent | Modal dialogs, consents and terms, sensitive-data prompts |
| language-and-locale | Language detection and switching, dates, numbers, text expansion |
| accessibility | The rules every screen meets: contrast, focus, targets, semantics, motion |
| content-and-tone | Writing for four languages: voice, length, terminology from jinnga-docs |
| iconography | Icon set, sizes, labels, meaning across cultures |
| motion | Durations, easings, reduced motion |
Adding a pattern: copy the shape of an existing one, add it to this table and to the documentation site navigation (site/nav.json).
Accessibility
Target
WCAG 2.2 level AA on web; the equivalent platform guidance on iOS and Android. Checked at design (this document), at build (token contrast checks) and at review (checklist below).
Rules built into the tokens
- Text contrast at least 4.5:1, large text and UI parts at least 3:1, in both themes; the build fails otherwise.
- Focus indicator
theme.focusat 3:1 against surfaces, 2px wide, offset 2px. - Touch targets at least 44px; spacing between targets at least
space.2. - Type roles with line height 1.5 for body text and generous sizes for accented scripts.
- Motion tokens collapse to zero when reduced motion is requested.
Rules for every screen
- Semantics: headings in order, landmarks, lists as lists, tables as tables, buttons as buttons.
- Names: every control and image has an accessible name in the current language; decorative images are hidden.
- Keyboard: everything operable by keyboard on web; visible focus; no traps except dialogs.
- Colour: never the only signal; status has an icon or text.
- Zoom and reflow: 200% zoom and 320px width without loss; text spacing overrides do not break layout.
- Time: no time limits without warning and extension; session expiry warns two minutes before.
- Errors: identified in text, linked to the control, with suggestions.
- Media: captions for video, transcripts for audio (relevant to wellbeing content).
Review checklist (per pattern or screen)
- Screen reader walkthrough in one language other than English.
- Keyboard-only walkthrough on web; switch control or voice on at least one mobile platform for critical flows (sign-in, consent, game session results).
- Contrast of any custom colour combination not covered by the tokens.
- Touch target measurement on the smallest supported phone.
Actions
Purpose
Make the next step obvious and the dangerous one safe.
Hierarchy
| Kind | Use | Style |
|---|---|---|
| Primary | The one action that completes the task on the screen | filled theme.primary, text theme.textOnPrimary |
| Secondary | Alternatives that do not complete the task | outlined theme.borderStrong, text theme.text |
| Tertiary | Low-emphasis actions, inline | text only, theme.primary |
| Destructive | Deletes, revokes, closes a contest | filled theme.danger with confirmation |
One primary per screen or dialog. Icon-only buttons have a tooltip and an accessible label.
Behaviour
- Pressed, hover (web), focused and disabled states are visible and distinct; loading replaces the label with a spinner and keeps the width.
- Destructive actions confirm with a dialog that names the object and the consequence; irreversible ones require typing a word or the object's name.
- Actions on a selection show the count ("Invite 12 students").
- Menus group related actions; destructive items go last, separated.
Accessibility
- Buttons are buttons; links are links. Minimum target
size.touchTarget. - Labels are verbs plus object ("Save changes", "Export results"), never "OK" or "Yes".
Language
- Verb-first labels in every language; reserve width for French and Portuguese; never abbreviate.
Tokens
theme.primary*, theme.danger, theme.textOnPrimary, size.control.*, radius.md, space.2, space.4, motion.duration.fast.
Content and tone
Voice
Clear, warm, respectful. The platform speaks to teachers, counsellors, parents, employees and students; it never speaks down, never alarms and never hides a consequence. It says what it does with data.
Rules
- Short sentences, one idea each. Active voice. Second person for instructions ("Choose a group"), first person plural only for the organisation's commitments.
- Terminology comes from the glossary in jinnga-docs, translated once per language and kept in the glossary of this package (
site/i18n/glossary.json): context, membership, group, game session, contest, wellbeing. The same term everywhere. - No jargon from the code (no "token", "payload", "entity"); no internal ids in messages meant for people.
- Numbers: digits, with locale formatting; dates relative when recent ("2 hours ago") and absolute otherwise.
- Sensitive topics (risk of dropping out, wellbeing) are described with care: the classification is explained, never a label alone, and the person's agency is stated.
- Errors name what happened and what to do; never blame.
Four languages
- Write English first, as the source. Translations are by meaning, not word by word; idioms are avoided in the source.
- Length: English labels are drafted at most 20 characters where they will be buttons or tabs; translations may run to 26.
- Formality: Spanish uses "usted" for institutional and company contexts and "tú" for household and student contexts; Portuguese uses "você"; French uses "vous" except in household and student contexts where "tu" applies. The context of the screen decides, not the language.
- Every string has a comment for translators describing where it appears and any variable it contains.
Microcopy table (reference)
| Situation | English source |
|---|---|
| Empty list, first use | "No students yet. Add the first one or import a list." |
| Save confirmation | "Changes saved." |
| Destructive confirmation | "Delete the group 'Grade 9B'? Its students keep their results." |
| Session about to expire | "You will be signed out in 2 minutes. Stay signed in?" |
| Consent decline | "You can decline. Nothing changes in your account, and you can accept later from your profile." |
Data display
Purpose
Show lists of students, groups, results, contests and invoices so that staff can find, compare and act, on a laptop and on a phone.
Choose the form
| Data | Form |
|---|---|
| Many rows, several comparable attributes, actions per row (students, invoices) | Table on md+; card list on phones |
| Few rows or one key attribute (contexts, games, plans) | Card list or simple list |
| One object in detail (a student's file) | Detail page with sections and tabs |
| Numbers over time or by category (results, risk) | Chart with a table alternative |
Table rules
- Sticky header; first column identifies the row and stays visible on horizontal scroll.
- Sorting on one column at a time, indicated by an icon and announced; default sort stated.
- Filters above the table as chips; active filters visible and removable; results count shown.
- Pagination of 25, 50, 100 rows with a stable position when returning from a detail; infinite scroll only on phones.
- Row actions in a menu at the end of the row; bulk actions appear when rows are selected, with the count.
- Empty, loading and error states follow the feedback pattern inside the table area.
- Sensitive values (wellbeing, risk classification) show only to roles allowed by the rules in jinnga-docs, and never in exports without the same check.
Cards on phones
- Title (first column), two to four key attributes as label and value pairs, status as a badge, actions in a menu; the whole card opens the detail.
Accessibility
- Real table semantics on web; on native, each row is one accessible element with the key values in its label.
- Sorting and selection are operable by keyboard; status is never colour only.
Language
- Numbers, dates and currency formatted per locale; column headers short and translated; allow header wrapping to two lines.
Tokens
theme.surface, theme.surfaceRaised, theme.border, space.3, space.4, radius.lg, shadow.sm, type roles label, body, bodySmall.
Dialogs and consent
Purpose
Interrupt only when needed, and record consent in a way that holds up: the platform handles minors, sensitive wellbeing data and employer relationships (ADR-002, ADR-003 and RN-IAM-009 and 010 in jinnga-docs).
Dialog types
| Type | Use | Size |
|---|---|---|
| Confirmation | A destructive or irreversible action | small, two actions |
| Form dialog | A short creation or edit that keeps the context behind | medium; long forms get a page instead |
| Consent | Terms, data use, sharing results with a context, wellbeing evaluations | medium, scrollable content, explicit accept |
| Sheet (phones) | Any of the above on xs and sm |
bottom sheet |
Consent rules
- The consent text is versioned and shown in the person's language; the version and language accepted are recorded.
- Accept is explicit: an unchecked box or a button labelled with what is accepted; never pre-checked, never "continue" meaning "accept".
- Decline is as easy as accept and explains the consequence without pressure; for voluntary evaluations, declining is not visible to the employer or institution (ADR-003).
- Minors: the dialog names who consents (guardian or institution) per the age rules in jinnga-docs.
- Consents can be reviewed and withdrawn from the profile; the dialog links there.
Behaviour
- Focus moves into the dialog and is trapped; escape and the close control dismiss non-consent dialogs; consent dialogs dismiss only by accept or decline.
- The background is covered by
theme.overlay; content behind does not scroll.
Accessibility
- Role dialog with a labelled title and description; the first focusable element is the least destructive action.
Tokens
theme.overlay, theme.surfaceRaised, shadow.lg, radius.xl, zIndex.modal, space.6, motion.duration.normal, motion.easing.enter.
Feedback and states
Purpose
Tell the person what the system is doing and what they can do next, in every state a screen can be in.
States of any content area
| State | Shows | Rule |
|---|---|---|
| Loading | Skeleton of the final layout | Never a blank area; skeleton after 200ms, spinner only for actions |
| Empty, first use | Illustration or icon, one sentence, the primary action to create the first item | Explains value, not absence |
| Empty, filtered | "No results for these filters" and a clear-filters action | Keeps the filters visible |
| Error, recoverable | What failed, in plain words, and a retry action | Never a code alone; the code goes in a details toggle |
| Error, not recoverable | What happened and whom to contact (support flow from jinnga-docs) | Reference id shown for support |
| Partial | Content plus an inline notice for the part that failed | The rest stays usable |
Notifications
- Inline: next to the thing it concerns (a field, a row, a section). Preferred.
- Toast: confirmation of an action just taken; disappears after 5 seconds, longer if it has an action; never for errors that need attention.
- Banner: system-wide conditions (maintenance, contract expiring) at the top of the content; dismissible when informational.
- Badge: counts on navigation items; never the only signal.
Accessibility
- Loading and result announcements use live regions; toasts are announced once and do not steal focus.
- Error summaries at the top of a form list the fields and link to them.
Language
- Messages are full sentences in the four languages; never concatenated fragments; counts use proper plural rules per locale.
Tokens
theme.success*, theme.warning*, theme.danger*, theme.info*, theme.surfaceRaised, shadow.lg, zIndex.toast, motion.duration.normal.
Forms
Purpose
Collect input from staff and from people in their homes without errors, on any device, in any of the four languages.
Anatomy of a field
Label (label role, above the control), control, optional helper text (bodySmall, textMuted), error text (bodySmall, danger) with an icon, required marker in the label text, not only an asterisk.
Rules
- One column. Two side-by-side fields only for pairs that belong together (first and last name) and only on
md+. - Labels are visible at all times; placeholders are examples, never labels.
- Validate on blur for format and on submit for completeness; never on every keystroke, except password strength.
- Errors are specific and say how to fix them: "Enter a date after today", not "Invalid".
- Long forms are split into steps with a visible progress indicator and a sticky action bar; every step saves a draft.
- Destructive or legal submissions (consents, deletions) confirm in a dialog that repeats what will happen.
- The primary action is one, aligned to the start of the form on web and full width on phones; secondary actions are text buttons.
States
Default, focused (focus ring in theme.focus, 2px offset), filled, disabled (textMuted, no interaction, still readable), read-only, error, success (only when confirmation matters, for example a verified email).
Accessibility
- Every control has a programmatic label; error text is linked to the control and announced.
- Control height
size.control.mdon web,size.control.lgon touch; spacing between controls at leastspace.4. - Keyboard: tab order follows reading order; enter submits from any text field; escape clears a search field.
Language
- Labels and errors are translated; formats (dates, numbers, identifiers) follow the locale; a national id field explains the expected format per country.
- Right-to-left is not required for the four languages.
Tokens
size.control.*, space.2 to space.6, radius.md, theme.border, theme.focus, theme.danger, theme.dangerSubtle, theme.textMuted.
Foundations usage
Purpose
Make every screen consistent by construction: components take their values from tokens, and tokens are organised so that the right choice is the easy one.
Rules
- Roles, not primitives. Components use theme roles (
theme.primary,theme.text,theme.surface) and type roles (body,label,heading2). Primitive scales (color.brand.500,fontSize.md) exist to define roles, never to style a component directly. The exception is data visualisation, which may use primitive scales for series. - Two themes, one set of roles. Light and dark expose exactly the same roles; the build fails otherwise. A component never checks which theme is active.
- Spacing on the 4px grid.
space.1tospace.24. Half steps (space.0.5) only for icon alignment. - Sizes for controls, not for text.
size.control.md(40px) is the default control height on web,size.control.lg(48px) on touch screens; the hit area is never belowsize.touchTarget(44px). - Type roles carry everything. A role sets family, size, weight, line height and letter spacing together; a component never overrides one of them.
- Elevation is a token.
shadow.smfor cards at rest,shadow.mdfor raised elements,shadow.lgfor overlays. On native, the same names map to platform elevation.
Anatomy of a token
color.brand.500 = #ff6a32 primitive, generated from the favicon orange in OKLCH
theme.light.primary = {color.brand.600} role, references a primitive
theme.dark.primary = {color.brand.400} same role, different primitive
Consumers: tokens.css exposes --color-brand-500, --theme-primary and .text-body; tamagui.tokens.js exposes color.brand500, themes.light.primary, textStyles.body.
Do and do not
- Do add a new role when two components need the same meaning (for example
theme.selected). Do not add a primitive to solve one component's problem. - Do name roles by meaning (
danger,textMuted), not by appearance (red,grey). - Do not hard-code a colour, size or font anywhere in the app; the lint rule rejects it.
Iconography
Set
One outline icon set with a consistent 2px stroke on a 24px grid, drawn on 2px increments, at the sizes size.icon.sm (16), md (20), lg (24), xl (32). Filled variants only to mark the active state of navigation items.
Rules
- An icon never stands alone without a text label or an accessible name; icon-only buttons have a tooltip on web.
- Meaning is checked across the four cultures: avoid hand gestures, mailboxes, national symbols and religious shapes; use neutral metaphors (calendar, people, chart).
- Status icons pair with colour roles: success check, warning triangle, danger octagon, info circle. The shape carries the meaning without colour.
- Game icons are provided by each game (games keep their own design); the platform shows them inside a neutral container with
radius.md. - Colour:
currentColorby default so icons follow the text role; status icons use their role colour.
Accessibility
- Decorative icons are hidden from assistive technology; meaningful ones have a name in the current language.
- Minimum rendered size
size.icon.mdinside interactive elements; the hit area is the control, not the icon.
Tokens
size.icon.*, theme.text, theme.textMuted, status roles, radius.md.
Language and locale
Purpose
Every interface works in English, Spanish, Portuguese and French from the first screen (product rule in jinnga-docs). The person is never stuck in a language they did not choose.
Supported locales
| Locale | Language | Notes |
|---|---|---|
| en-US | English (default) | Fallback for any missing string |
| es-419 | Spanish, Latin America | Neutral regional Spanish; not es-ES |
| pt-BR | Portuguese, Brazil | |
| fr-CA | French, Canada |
Precedence for the active language
- The language saved in the identity's profile (rule in jinnga-docs IAM).
- The language chosen in this session or on this device.
- The device or browser preference (
Accept-Language, device locale). - The country of access (provided by the edge), as a last hint.
- English.
The switcher is always reachable: top bar on web, settings and the sign-in screen on mobile. Changing the language applies immediately, without reload, and offers to save it as the default.
Text expansion and layout
- Reserve 30% beyond English for labels and buttons; let headings wrap; never truncate labels by default.
- Test every component in the four locales; the documentation site shows each sample string in all four.
Formats
- Dates, times, numbers, currency and lists use the locale's conventions through the platform's international formatting APIs; never hand-formatted.
- Names and national identifiers are not translated; their input format follows the country, not the language.
- Plurals and gender use ICU message syntax; no string is built by concatenation.
Content that is not translated
Text written by institutions or people (announcements, observations, names) is shown as written, with its language tagged so assistive technology pronounces it correctly.
Accessibility
- The document and each foreign-language fragment carry a language attribute; the switcher lists languages in their own names (Español, Português, Français, English).
Tokens
None specific; see typography for line heights chosen for accented scripts.
Layout
Purpose
One structure that works from a 360px phone to a 1536px desktop, for a panel used by institution staff and for apps used by students, parents and employees.
Breakpoints
| Name | Min width | Typical device | Layout |
|---|---|---|---|
| xs | 0 | phone portrait | single column, bottom navigation |
| sm | 480px | phone landscape, small tablet | single column, wider gutters |
| md | 768px | tablet | two columns where content allows; side navigation collapsible |
| lg | 1024px | laptop | side navigation visible; content up to size.container.md |
| xl | 1280px | desktop | content up to size.container.lg; secondary panels |
| 2xl | 1536px | wide desktop | content capped at size.container.xl, centred |
Anatomy
- App shell: top bar (context switcher, language, account), navigation (side on
lg+, bottom onxstomd), content area, optional secondary panel. - Page: title block (heading1, optional description, primary action), content sections separated by
space.8, sticky action bar on long forms. - Gutters:
space.4onxs,space.6onmd,space.8onlg+. - Grid: 4 columns on
xs, 8 onmd, 12 onlg+; gapspace.4.
Behaviour
- Content never reflows on orientation change in a way that loses position or input.
- Side navigation collapses to icons with tooltips on
md; labels stay onlg+because four languages need the width. - The secondary panel (details of a selected row) slides over content below
lgand sits beside it onlg+.
Accessibility
- Reading order equals visual order; the navigation comes before the content in the DOM and has a skip link.
- Zoom to 200% keeps every control reachable; no horizontal scroll at 320px width.
Language
- Reserve 30% more width than the English label needs in navigation, tabs and buttons; test with French and Portuguese strings before fixing a width.
Tokens
size.container.*, breakpoint.*, space.4 to space.8, zIndex.sticky, shadow.sm.
Motion
Purpose
Motion explains change: where something came from, where it went, what is happening. It never decorates.
Durations and easings
| Use | Duration | Easing |
|---|---|---|
| State change of a control (hover, press, focus) | motion.duration.fast (120ms) |
standard |
| Appear, disappear, small moves (toast, menu, tab indicator) | motion.duration.normal (200ms) |
enter for appearing, exit for leaving |
| Layout change, sheet or dialog, navigation transition | motion.duration.slow (320ms) |
standard |
| Celebratory feedback (contest result, badge earned) | motion.duration.deliberate (480ms) |
spring, once |
Rules
- Reduced motion: every duration becomes instant and springs become standard; the state change still happens, without animation.
- Nothing loops indefinitely except a loading indicator, and that one pauses when the element is not visible.
- Motion never blocks input: a person can act while a transition plays.
- Enter and exit are asymmetric: leaving is faster than arriving.
- Skeletons shimmer slowly (1.5s) and respect reduced motion.
Accessibility
- No flashing above three times per second; no parallax tied to scrolling on content pages.
Tokens
motion.duration.*, motion.easing.*.
Navigation
Purpose
Let a person move between the sections of their context (institution, company, household, platform) and switch context without losing where they were.
Anatomy
- Primary navigation: the sections of the active context, at most seven. Side bar on
lg+, bottom bar with up to five items plus "more" on phones. - Context switcher: in the top bar; lists the contexts the identity belongs to (ADR-001 in jinnga-docs) with role and name; the active one is always visible.
- Breadcrumb: on
md+for depth 2 or more; on phones, a back control with the parent's title. - Tabs: for views of the same object (a student's overview, results, wellbeing); never for navigating between objects.
Behaviour
- The current item is marked by colour (
theme.primary), weight (label role) and an indicator; never by colour alone. - Switching context returns to that context's home; the previous context's state is kept for the session.
- Deep links open the exact screen; if the identity lacks the context, the app explains it and offers the available contexts.
Accessibility
- Navigation is a landmark with a label; items are links on web and buttons with role tab on native tabs.
- Focus moves to the page title after navigation; screen readers announce the section change.
Language
- Section names come from the glossary in jinnga-docs translated once per language; the same word is used in navigation, titles and breadcrumbs.
Tokens
theme.primary, theme.text, theme.textMuted, size.touchTarget, space.3, radius.md, zIndex.sticky.