Gives an Expo app one shared component library in src/ui/: forty presentational components
listed in a repo-owned catalogue (contracts/component-library.json), each sized from specs
generated from the token file, pressed through the one press surface, showing every state with a
cue beyond colour, named for VoiceOver and TalkBack, addressed by a required testID whose parts it
derives itself, and proven by its own test. Two zero-dependency scripts prove the catalogue, the
specs and the component rules, including the device lessons for iOS and Android.
Rules that must hold
- The catalogue lists every shared component, with its parts and the looks it may have. Each
src/uicomponent is an entry (component-missing,component-unlisted) and exportsfunction <Name>andtype <Name>Props(component-export). Why: a checker can only hold what it knows, and a flag (mayShadow,mayRotate...) allows a look exactly where it is meant. - Measurements come from the generated specs, never a typed number. The token file's
componentsblock is the source;write-component-specs.mjswritescomponent-specs.jsonand components readCOMPONENT_SPECS(specs-missing,specs-mismatch,measurement-literal); an override carries its reason (spec-override-reason). Why: a retyped value drifts by one digit and nobody notices until a screen looks wrong. - One press implementation. Every pressable composes design-system-structure's
PressableSurface; noPressable, Touchable, coreButtonor gesture-handler button anywhere else (press-outside-host; a provider-free crash fallback states why in a// press-outside-host: <reason>comment), no hand-written pressed look (own-press-look), and each press host runs the injected feedback, except a switch, whose handler plays its own (press-feedback). Why: every control then answers, sounds and announces the same way, and the look is one token. - Every component a test or flow addresses takes a required
testID, derives its parts from it, and puts it on the box the design measures (testid-required,part-not-derived,testid-on-inner-text; the grammar is naming-conventions'). Why: screens never build part ids by hand, and on iOS a testID inside an accessible element vanishes from the tree E2E tools read. - Every pressable has a role, a translated name and its state; decorative parts hide themselves
from VoiceOver and TalkBack with both props (
decorative-not-hidden). Why: a screen reader then reads one element ("Daily reminder, switch, on"), not a list of pieces. Touch targets andhitSlopare accessibility-as-code's rules. - Components are presentational. Props in, JSX out: no translation API (
ui-translates), no stores or services (architecture-and-boundaries), strings arrive translated, numbers formatted,isReducedMotionfrom the screen model (motion-and-animation's hooks, once adopted, read it themselves), icons through anIconSlot. Why: a component stays testable without the app and works in every language and icon set. - Shape before colour, and a test holds each cue. Every state keeps a non-colour cue: a check, a
written state, a wider dot, a bold edge, a dash. Each cue has a component test that fails when
the cue is removed (a derived part such as
.check, ortoHaveStylefor a width or an edge). Why: about 1 in 12 men cannot tell the status colours apart, and an untested cue disappears in the next restyle while a colour-only assertion stays green. - Flat by default. Shadows and rotation only on components the catalogue marks
(
shadow-outside-allowed,rotate-outside-allowed). Why: a shadow says "floats" or "press me"; on a card it promises an action that is not there. - Motion has a reduced alternative, and a functional timer ignores reduce motion. Fades instead
of drops, nothing instead of confetti; the hold timer passes
ReduceMotion.Never(hold-reduce-motion). Why: under Reduce Motion Reanimated finishes a timing at once, and the hold would confirm on touch. - Faces grow with their text, and cells grow without a zero basis (
fixed-text-height,pair-cell-flex). Why: a fixed height clips at 200 % text, andflex: 1in a column cell collapsed a row of keys to 0 pt on a device while every Jest test passed. - The scrim is the modal root: it renders in React Native's Modal, carries
accessibilityViewIsModal, and closes on Android back (modal-root,modal-back). Why: a modal card hid the scrim's testID from the tree, and a dialog that ignores back traps Android users. - The device lessons hold: underlines are drawn bars (
drawn-underline), a hold fill sits in a track pinned to the face edges (hold-fill-track), a scatter lays out left to right (scatter-ltr). Why: no platform sets an underline's thickness, Yoga takes an absolute child's percentage inside the padding, andstartoffsets mirror in right-to-left. - Every component has a test rendered through
renderWithProviders, asserting role, name, state and parts, ending with thefindInaccessiblePressablesaudit (component-untested). Why: a missing role or a renamed part then fails a test, not a person or a flow. - Never build a design's stand-ins (an ad box, an OS consent sheet) listed under
mockOnly(mock-only-built). Why: a stand-in ships as a fake.
Workflow
- Look first. From the repo root run
node ${CLAUDE_SKILL_DIR}/scripts/check-components.mjs .. NOT APPLICABLE means no library yet;catalogue-missinglists the app's existingsrc/uicomponents and the file rules they break: that is the work list. Read references/component-catalogue.md. - Prerequisites. The theme, AppText, PressableSurface and the press feedback context come from
design-system-structure;
test/render-with-providers.tsxfrom unit-and-component-tests;test/a11y/find-inaccessible-pressables.tsfrom accessibility-as-code.prerequisite-missingnames any that is missing and who creates it. - Measurements. Merge the
componentsblock ofassets/components-block.jsonintodesign/tokens.json(the canonical demo token file may already carry the same block) and replace the values with the design's (references/measurements-and-specs.md); then runnode ${CLAUDE_SKILL_DIR}/scripts/write-component-specs.mjs .. - Copy the library. From
templates/to the same paths:contracts/component-library.json(and its schema),src/ui/component-specs.ts,icon-slot.ts,variant-paint.ts,use-hold-to-confirm.ts, and each component the app needs with its test (a component's own imports name the others it needs). Remove catalogue entries the app does not adopt. Keep the values; adapt only what the app's design changes, reading references/actions-and-inputs.md or references/lists-surfaces-and-overlays.md first. When the design asks for platform-native controls instead, hand off to Expo'sexpo-uiskill. - Moments. When the app has motion-and-animation's moment catalogue, declare the Toggle,
Toast and hold moments with those files in
files, swap BusyIndicator for examples/busy-indicator-with-moments.tsx and drop Confetti for<CelebrationBurst>(building-a-component.md, "With motion-and-animation's moment catalogue"); then itscheck-motion.mjspasses onsrc/ui/. - Contrast. Each colour on a new ground is a pair in
contracts/contrast.json(accessibility-as-code). Beyond the canonical palette pairs, the templates paintprimaryonsunken(the progress and slider fills): add it undershapes, then run accessibility-as-code'scheck-contrast.mjs. - Wire into screens. The screen model builds strings with
t(), formats numbers, reads reduce motion and passes base ids; imitate examples/reminder-settings.tsx and its test. testIDs and roles: references/testids-and-accessibility.md. - A new component follows references/building-a-component.md: one file, specs, semantic paint, AppText, the press surface, a required testID with derived parts, a test, a catalogue entry with its flags, its measurements in the token file. The device traps: references/layout-and-platform-lessons.md.
- Older primitives (a
PrimaryButtonon a raw Pressable): move each caller to the library component, then delete the old file and say so in the report (building-a-component.md, "Replacing older primitives"). - Run the checks from the repo root until all pass:
npm run typecheck,npx eslint . --max-warnings 0,npx jest --ci, then the two scripts below (use--only <Name>while building a subset, never at the end). Every FAIL line names the file, the rule and the fix; references/catalogue-contract-and-checks.md explains each rule and every catalogue key. - Report in plain words which components were added or changed, which checks passed, any override and its reason, files deleted, and the open questions touched (references/defaults-and-open-questions.md). Ask the reviewer before changing a value the design fixed.
Definition of done
-
contracts/component-library.jsonlists everysrc/uicomponent with its parts and flags, and nothing the app does not have. - The token file has the design's
componentsblock; overrides (if any) each give a reason. - Each component composes the press surface (no other press, no own pressed look), takes a
required testID and derives its parts, hides its decorative parts, reads every size from
COMPONENT_SPECSor a token, and has a test that ends with the pressable audit. - Every non-colour state cue has a test that fails without it: removed once, the test went red, then the cue was put back.
- The pairs the components paint are in
contracts/contrast.jsonand pass. -
npm run typecheck,npx eslint . --max-warnings 0andnpx jest --cipass (iOS and Android projects). - No SKIP lines are expected; NOT APPLICABLE only while the app has no library yet.
-
node ${CLAUDE_SKILL_DIR}/scripts/write-component-specs.mjs . --checkprintsRESULT: PASS -
node ${CLAUDE_SKILL_DIR}/scripts/check-components.mjs .printsRESULT: PASS
Anti-patterns
- A quick
TouchableOpacityor a({ pressed }) => ({ opacity })style. Compose the press surface or use Button, ListRow, IconButton; change the look inPRESS, once. - A shadow on a card to make it look important. It promises a press; put a button in the card or use ActionCard.
- A part id built in the screen (
'tasks.add-button' + '.label') or an index key (${testID}.${index}). Pass the base id; key repeated items by data. t()or a store insidesrc/ui. The screen model builds the strings and reads the stores.- Editing
component-specs.jsonto make a layout fit. Fix the layout; change a measurement in the token file, or add an override with the design's reason. - A testID on the text inside a chip, badge or header. E2E tools then measure only the words; put
it on the View with
accessible, role and label. flex: 1on a tile in a grid cell. A zero vertical basis; use{ flexGrow: 1 }.- Centring a badge with the parent's
alignItems. Its ownalignSelfwins; wrap it in a row withjustifyContent: 'center'. accessibilityViewIsModalon the dialog card, or a dialog drawn in the tree with no back handling. Use the Scrim: Modal, the flag on the scrim,onRequestClose.textDecorationLinefor a designed underline. Draw the bar.- A percentage width on an absolute child of a padded face. Put it in a track pinned to the edges.
- Confetti placed with
start: xand nodirection: 'ltr'. It mirrors in Arabic or Hebrew. - A fixed
heighton a face that holds text. UseminHeight. - A hold-to-confirm that finishes under Reduce Motion, or that only a finger can do. Pass
ReduceMotion.Never; offer the activate action. - Labelling every icon inside a row, or un-hiding a decorative part to measure it. The row carries the name; give the measured element the id.
- Calling the sound or haptic API from a component or screen for a press. The press host already plays the tap, so a second call doubles it; a switch's handler plays its own toggle feedback, and what plays is audio-and-haptics-feedback's choice.
- Building the design's ad box or consent sheet. The SDK and the OS draw the real ones.
- Treating a green checker as proof a screen looks right. It proves structure and rules; looks are proven on device screenshots (design-visual-parity).
Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| references/component-catalogue.md | The forty components (file, role, parts, flags), support files, prerequisites, other owners, mock-only parts, games, when multi-package | Workflow step 1, every time |
| references/actions-and-inputs.md | The press model; buttons, hold, tiles, cards, checkbox, toggle, segmented control, slider, option card, progress bar: anatomy, states, accessibility | Changing or wiring a pressable or an input |
| references/lists-surfaces-and-overlays.md | Lists and rows, tiles, cards, callouts, badges, chips, app bar, scrim and dialogs, toasts, empty state, stats and charts, drawn marks, confetti | Changing or wiring a flat part or an overlay |
| references/building-a-component.md | File anatomy, specs and paint, the icon slot, composing the press surface, motion, the test, lint limits, the catalogue entry, replacing older primitives, the moment catalogue table | A new component; workflow steps 5, 8 and 9 |
| references/measurements-and-specs.md | The components block, references, the generator, reasoned overrides, browser-rendered borders, turning specs off | Workflow step 3; any measurement question |
| references/testids-and-accessibility.md | Required testIDs and derived parts per component, ids on measured boxes, what E2E tools see on iOS and Android, decorative parts, roles, the pressable checklist | Wiring screens, writing tests |
| references/layout-and-platform-lessons.md | The device traps behind the layout rules, on iOS and Android: pair cells, alignContent, alignSelf, absolute percentages, underlines, scatters, fixed heights, the modal root and back, directions, press looks | Before changing layout code |
| references/catalogue-contract-and-checks.md | Every catalogue key and flag, options and rn-skills.config.json keys, every rule with its fix, passing output, adopting in an existing app | Editing the catalogue; reading a FAIL line |
| references/defaults-and-open-questions.md | Defaults the templates choose and why, states to decide, what to verify on devices | Reporting; before changing a default |
templates/src/ui/ |
The forty components, each with its test (scrim.android.test.tsx for Android back); component-specs.json and .ts; icon-slot.ts; variant-paint.ts and use-hold-to-confirm.ts with their tests |
Workflow step 4 (copy) |
templates/contracts/ |
component-library.json (the catalogue) and component-library.schema.json |
Workflow step 4 (copy, then edit) |
| examples/reminder-settings.tsx | A feature's settings group built from ListGroup, ListRow, SubRow and Slider with base ids and props from the model | Workflow step 7 |
| examples/reminder-settings.test.tsx | Its test: switch, adjustable, hidden row, audit | Workflow step 7 |
| examples/busy-indicator-with-moments.tsx | BusyIndicator looping through motion-and-animation's useMotionLoop (same props; the template's test passes on it) |
Workflow step 5, when the app has the moment catalogue |
assets/components-block.json |
The demo component tier the templates were built from | Workflow step 3 (merge into the token file) |
assets/shared.json |
Declares the synced files: check-lib, the source scanner, the canonical demo token file the fixtures use | When adding a shared file |
scripts/check-components.mjs |
Checker: catalogue, prerequisites, specs, files, exports, testIDs and parts, tests, one press implementation, the component and device rules | Workflow steps 1 and 10, and at the end |
scripts/write-component-specs.mjs |
Generator: component-specs.json from the token file (--check compares and names the first difference and why) |
Workflow step 3, and at the end |
scripts/lib/component-source.mjs |
Catalogue loading and validation, the spec derivation, StyleSheet and JSX scanners | When a rule needs changing |
scripts/lib/source-scan.mjs |
Shared import scanner and resolver (synced, do not edit here) | Never by hand |
scripts/check-lib.mjs |
Shared script helper (synced, do not edit here) | Never by hand |
scripts/selftest.mjs |
Proves both scripts on the shipped templates and every planted bug | After changing a script, a template or a fixture |
tests/ |
The base app (prerequisite stubs, the synced token file) and overlay cases: good, one bad case per rule, pass and error cases | When adding a rule |
Related skills
design-system-structure- tokens, themes, AppText, PressableSurface and the press look these components build on.accessibility-as-code- touch targets, hitSlop, contrast pairs, reduce motion, overlays and screen-reader checks app-wide.naming-conventions- the testID grammar, file and export names.unit-and-component-tests- renderWithProviders, test forms and the Jest set-up.react-components-and-hooks- component and hook rules, effects, lists.architecture-and-boundaries- whysrc/uiimports no stores or services.icons-app-icon-and-splash- the icon set the icon slots draw from.forms-and-input- text fields and form controls, which join this catalogue.ui-states-and-feedback- when to show an empty state, a toast, a banner or a dialog.screen-contracts- screens composed from these components and their ids.design-visual-parity- proves a built screen matches its approved design.motion-and-animation- the app's moment catalogue and celebrations.audio-and-haptics-feedback- what the press feedback plays.rtl-and-direction- direction switching, mirroring and bidi.e2e-maestro- flows that select these testIDs on iOS and Android.game-session-host- game parts (level tiles, star ratings, a game's top bar) built to the same rules.- Expo's
expo-uiskill - platform-native SwiftUI and Jetpack Compose controls (@expo/ui).