Components
Language: JavaScript
The component library ships atoms, molecules, composites, and providers: themeable, accessible primitives built on React Native Web. The library is own code, following Superloom's loader pattern and Lib DI throughout. Design languages arrive as themer template packs, not as separate component libraries. This page defines the component vocabulary, the authoring contract, the four-bucket exception model, and the accessibility contract.
On This Page
- Component Vocabulary
- Atom Set
- Molecule Set
- Composite Set
- Provider Set
- Authoring Contract
- Theme Token Contract
- Named Barrel
- Utility-Class Mapping
- Four-Bucket Exception Model
- Interaction States
- Geometry and Fidelity Contract
- Accessibility Contract
- Generic vs Custom
- Peer Dependencies
- Further Reading
Component Vocabulary
The library uses four tiers. Atoms and molecules follow Brad Frost's atomic design taxonomy. Composites extend the hierarchy for components that compose other molecules (MenuButton composes Menu which composes MenuItem). Providers are context-only components that render no visual output.
| Tier | Directory | Definition | Boundary |
|---|---|---|---|
| Atom | atom/ | An irreducible primitive wrapping one RN element with token consumption and accessibility behavior | No composition of other library components. No domain knowledge |
| Molecule | molecule/ | A composition of atoms with interaction logic | No domain knowledge. Composes atoms only; never composes other molecules |
| Composite | composite/ | A composition of atoms, molecules, and other composites with coordination logic | No domain knowledge. May compose atoms, molecules, and other composites |
| Provider | provider/ | A context-only component that renders no visual output and consumes no tokens | No visual output. Registered at Component.provider.[name] |
Organisms and above are not library concepts. Anything domain-aware (a product card, a cart summary, a checkout form) is an app-side screen component or an app-registered variant. It never ships in the component library. This bounds the library and answers the question: there is no organism/ folder because organisms are app concerns.
The composite tier exists because real design systems have deeper composition chains than atom-molecule can express. A DataTable composes Table which composes TableRow which composes TableCell. The boundary that matters is unchanged: no domain knowledge. A composite is still generic. A product card and a checkout form remain app concerns.
The four-bucket exception model (below) handles components that deviate from the canonical set.
Atom Set
The canonical atom set wraps primitive React Native elements. Each atom maps props to utility classes and applies accessibility behavior.
| Atom | RN element | Key props |
|---|---|---|
View | View | Layout, background, padding, margin |
Text | Text | Size, color, weight, family |
Button | Pressable | Variant, size, state, onPress, accessibilityLabel |
Icon | Text (vector icon) | Name, size, color |
Image | Image | Source, resize, aspect |
TextInput | TextInput | Value, placeholder, state, accessibilityLabel |
Toggle | Switch | Value, onValueChange, state |
Tag | View | Label, color, dismissible |
BadgeIndicator | View | Count, color, position |
ProgressBar | View (animated) | Value, color, size |
Adding an atom is a library change. The atom must follow the authoring contract, consume tokens through utility classes, and include accessibility behavior.
Molecule Set
Molecules compose atoms with interaction logic. A molecule coordinates state across its child atoms but carries no domain knowledge.
| Molecule | Composes | Interaction |
|---|---|---|
Button | Icon + Text + Pressable | Hover/press/disabled state resolution, icon + label layout, kind prop (primary, secondary, tertiary, danger, ghost) |
Dropdown | Button + View + Text | Open/close state, selection, accessibility focus management |
Modal | View + Text + Button | Visibility state, backdrop, focus trap |
Card | View + Text + Image | Layout, optional press state |
ListItem | View + Text + Icon + Separator | Selection, swipe actions, accessibility role |
Adding a molecule is a library change. The molecule must compose atoms only, consume tokens through utility classes, and include accessibility behavior for its interaction pattern.
Composite Set
Composites compose atoms, molecules, and other composites with coordination logic. A composite coordinates state across its children through React Context, not through prop drilling or React.Children.map. Examples include Tabs, Accordion, Menu, DataTable, RadioButtonGroup.
| Composite | Composes | Coordination |
|---|---|---|
Menu | MenuItem, View | Context for active item, roving tab index |
Tabs | Tab, TabList, TabPanel | Context for active tab, aria-controls wiring |
Accordion | AccordionItem | Context for expanded state |
DataTable | Table, TableRow, TableCell | Headless render-prop API for sort/select/expand |
Adding a composite is a library change. The composite must use Context for parent-child coordination (never React.Children.map plus cloneElement, which breaks when children are wrapped in React.memo or forwardRef). Contexts are created once per loader instance, not inside build, so a rebuild does not orphan mounted Consumers.
Provider Set
Providers are context-only components that render no visual output and consume no tokens. They are registered at Component.provider.[name], matching the existing Component.variant and Component.freeform namespacing. They do not count toward the flat top-level key count.
| Provider | Purpose |
|---|---|
Overlay | Overlay stacking and z-index management |
LiveRegionProvider | Screen reader announcements through aria-live regions |
Layer | Elevation level context for nested surfaces |
Theme | Theme override context for subtrees |
FeatureFlags | Feature flag context for conditional rendering |
IdPrefix | ID prefix context for scoped id generation |
FluidForm | Form-level context for fluid label positioning |
ErrorBoundary | Error boundary for component subtrees |
Adding a provider is a library change. The provider must be a Context provider, not a visual component. ErrorBoundary is the one component in the package that must be a class, because componentDidCatch has no hook equivalent.
Authoring Contract
The library entry point is createSystem(shared_libs, config, theme, breakpoint), which builds the shared infrastructure, registers every component factory, and returns a themed component registry. The system is the only entry point; there is no separate loader or build function.
The contract:
createSystemgeneratesCommonStyle(the utility style map for the current theme) and wires every component viamake(factory)- Each component factory is
function (Lib, CONFIG, ERRORS, Parts, Registry, Style)returning a React component - The component maps props to utility classes:
sizetofont_size_[step],colortofont_[token],weighttofont_weight_[name] - Molecules compose atoms through the shared
Componentobject, not through direct imports - Directional layout uses
Parts.Direction, a context-based direction provider. Components that need the writing direction callParts.Direction.useDirection(). Logical style properties (paddingStart,paddingEnd,marginStart,marginEnd) mirror automatically under RTL and require no manual intervention. Directional icons useParts.Directionwith amirrorprop andtransform: [{ scaleX: -1 }]
Re-theming calls createSystem with a new theme, which re-derives CommonStyle and returns a fresh registry. The previous registry is never mutated; callers swap the reference. This is the runtime re-theming mechanism.
Consumption Pattern
Component factory files use export default function (Lib, CONFIG, ERRORS, Parts, Registry, Style) { ... }, and createSystem imports them via import viewFactory from './component/atom/view.js'. The system entry point uses export function createSystem (shared_libs, config, theme, breakpoint). See Module Structure for the full skeleton.
Consumers that use a bundler (Vite, Metro) import createSystem directly.
Parts and the Style Contract
The loader builds Parts once per instance from parts/. Components never import a mechanism directly; they receive Parts through their factory signature. Each part loader takes the mandatory uniform signature (shared_libs, config, errors).
Style is PascalCase with no trailing underscore, following the casing table for internally-assembled namespaced containers. It carries utilities, tokens, breakpoint, and allBreakpoints.
Internal constants live in data/style-contract.json, not in config. Unit-conversion factors, numeric style property lists, and precision values are intrinsic data, not overridable configuration.
Theme Token Contract
A component system requires its tokens from the Superloom token contract and declares the subset it requires and the subset it supports as exported data. createSystem calls Themer.validateContract with both lists: missing required tokens are one TypeError naming them all; unsupported provided tokens are one warning naming them all. No component source contains a color literal, reads a token by a name outside the contract, or falls back from one token to another; CI gates G24, G27, G28, and G29 enforce this. A hardcoded fallback would make an incomplete theme look complete while substituting the library's own design decisions; the correct behavior is to refuse to build so the theme author sees the gap.
Named Barrel
A public barrel exposes named exports and no default export. This lets a bundler tree-shake unused components and forces consumers to import an explicit surface rather than receiving an opaque default.
A default export reintroduces a second surface that the barrel's named exports already cover. It defeats tree-shaking because the bundler cannot prove which named bindings the default carries. The rule: the package root and any registration barrel export named bindings only.
Utility-Class Mapping
Components read named utility classes rather than inline token lookups. The mapping is deterministic:
| Prop | Utility class | Example |
|---|---|---|
size | font_size_[step] | size="md" to font_size_md |
color | font_[token] | color="text_primary" to font_text_primary |
weight | font_weight_[name] | weight="semibold" to font_weight_semibold |
background | background_[token] | background="surface" to background_surface |
padding | p_[side]_[step] | padding="a_md" to p_a_md |
margin | m_[side]_[step] | margin="t_lg" to m_t_lg |
radius | br_[step] | radius="pill" to br_pill |
Spacing utilities use logical sides (s/e) for RTL. See Theming for the full utility style reference.
Four-Bucket Exception Model
Real apps need disciplined deviation and a clean way to abandon the token system entirely. The four-bucket model handles both.
| Bucket | Location | Token access | Registry | Re-themes |
|---|---|---|---|---|
| Canonical | atom/, molecule/, or composite/ | Full token access via CommonStyle | Component.[name] | Yes |
| Provider | provider/ | No token access. Context only | Component.provider.[name] | No |
| Structured variant | variant/ | Full token access via CommonStyle | Component.variant.[name] | Yes |
| Unstructured freeform | freeform/ | No token access. No CommonStyle. Raw styles only | Component.freeform.[name] | No |
Canonical
The default. Atoms, molecules, and composites reading tokens through utility classes. This is the normal case.
Provider
A context-only component that renders no visual output and consumes no tokens. It lives in Component.provider. It does not re-theme because it has no visual output to re-theme. See Provider Set for the full list.
Structured variant
A different composition of the same atoms with the same tokens. Example: an outlined button variant shares Button + Text atoms but changes the background and border resolution. The variant is registered in Component.variant so it is discoverable. It re-themes when the theme changes because it reads the same CommonStyle.
Unstructured freeform
A component that opts out of the token system entirely. It receives no CommonStyle, no theme, no tokens. It takes raw styles only. It lives in Component.freeform, a fenced namespace. It does not re-theme.
The freeform bucket exists for components that cannot conform: a marketing hero, a chat bubble, a one-off animation. A future lint rule flags imports from freeform/ so its use is a conscious decision.
The rules are testable constraints:
- A canonical component must not import from
variant/orfreeform/ - A provider component must register in
Component.providerand render no visual output - A variant component must register in
Component.variant - A freeform component must not receive
CommonStyleortheme - A freeform component must not appear outside
Component.freeform
Interaction States
Every interactive component supports a set of states. The state names are the standard interaction-state vocabulary. Some states are persistent (selected, checked, current, expanded, invalid), others are transient (hovered, pressed, focused). A component can be in multiple states simultaneously; the precedence rules are component-specific and documented per component family.
| State | Meaning | Visual treatment |
|---|---|---|
enabled | Default resting state | Base token values |
hovered | Pointer over the component | pseudoHover color operation (lightens dark colors, darkens light ones) |
pressed | Component is being pressed | pseudoPress color operation (stronger shift than hover) |
focused | Component has keyboard or screen-reader focus | Focus ring or outline |
disabled | Component is non-interactive | disabled color operation (45% original + 55% white) |
loading | Component is performing an async action | Non-interactive, announces aria-busy, renders a Loading or Skeleton |
selected | Component is the active choice in a group | Authored selected token, not a pseudo-state derivation |
checked | Toggle/checkbox is on | Authored checked token |
current | Component marks the current page/step | Authored current token |
expanded | Component reveals additional content | aria-expanded semantics, visual indicator |
invalid | Component has a validation error | Authored error/invalid token |
The themer engine derives pseudo-state colors from base colors through the template's color operations. However, a design system may provide authored values for selected, checked, current, and invalid states rather than deriving them from pseudoHover or pseudoPress. The template declares which approach each token uses. A component must not assume a derived value when the template provides an authored one.
Selected is not pressed. A selected tab or navigation item retains its selected treatment at rest. A pressed state is a transient pointer-down visual. A component can be selected and pressed simultaneously; the selected indicator persists while the pressed fill overlays. A border indicator (such as a top border on a selected tab) is not equivalent to a background fill and must not be substituted for one unless the pinned design system specification explicitly calls for an indicator border.
The focused state is the accessibility-visible state. It must render a visible focus indicator on every platform, including web (keyboard navigation) and native (VoiceOver/TalkBack focus).
Geometry and Fidelity Contract
A component's geometry (heights, paddings, border sides, icon sizes, target sizes, radius tokens, glyph names) is declared data, not an emergent result of padding and line height. The declaration is shared by the component and its tests, so a rendered size that drifts from the declaration fails a test instead of waiting for a human to notice. The normative visual specification is the pinned design system the template reproduces; the library expresses it in contract tokens and never in literals.
Spec Sheets
data/component-spec.js holds one frozen sheet per covered component and Parts.Spec(name) returns it. A sheet names geometry through token references (heightToken, radiusToken, iconSizeToken) wherever contract tokens exist, and a field-specific rawReason where no token covers the value. A sheet never carries a bare number without a token reference or a reason. Components read the sheet; they do not repeat the values. Three tests hold the sheet honest:
- Spec validation: every token name in a sheet resolves in the strict utility registry, and every geometry value matches the oracle generated from the pinned design system package.
- Geometry lint: a numeric literal for a size, padding, or radius in component source is a defect. The lint resolves token references before comparing, so a sheet that reads
heightToken: 'size.size_medium'is checked at 40, not at the token name. - Assertion integrity: each permanent test is paired with a way to disable the behavior it guards; the manifest proves the test fails when the behavior is off. A test that cannot be made to fire is not a gate.
A spec-coverage.js manifest lists every component as specced or unspecced with a reason; a component represented in either oracle may not remain unspecced. The unspecced count may not grow.
Component Geometry Oracles
Geometry in a component library is declared data, and a raw number in a component library is one design system's opinion hardcoded. Every geometry value is a token reference; the template supplies the number. An oracle validates that the spec sheet agrees with the design system the template reproduces.
A geometry oracle is generated from the pinned published packages of one design system and validates that system's defaults only. Carbon resolves in two stages: the layout scales are parsed from @carbon/layout SCSS, and each component's default size and density are parsed from its own @carbon/styles component SCSS (layout.use('size', $default: ...)). Material's per-component tokens are parsed from @material/web/tokens/versions/<pin>/_md-comp-*.scss.
The method taxonomy is part of the contract: every entry is parsed, inherited, transcribed, or none, and the last two carry a reason. State plainly why: a transcribed value cannot detect upstream drift, so it is a declared weakness rather than an invisible one.
A deviation is declared, never assumed. Where the implementation differs from a system's default, the spec sheet carries sizeChoice naming each system's value and the reason. Use the real case as the worked example: Carbon's button default is lg (48px) and this library ships 40px (size="md").
A custom template needs no oracle. The check for a scratch-built template is contract validity - the token exists and resolves - not equality with an external authority. This is what makes the method template-agnostic.
The twelve-check matrix:
| Check | Rule |
|---|---|
| C1 | Pin lock: the oracle records the source package version |
| C2 | No transcription without a reason: every transcribed entry carries a reason |
| C3 | Parse coverage floor: at least textInput, button, search, tag, select report parsed |
| C4 | Idempotency: regenerating the oracle produces a byte-identical artifact (minus timestamp) |
| C5 | Self-check constants: known shape and size values are asserted at generation time |
| C6 | Ledger completeness: every ledger component has a method |
| C7 | No orphans: a component in either oracle may not remain unspecced |
| C8 | Spec-versus-oracle agreement: spec sheet geometry matches the oracle for parsed entries |
| C9 | Cross-oracle declaration: disagreements between Carbon and Material are declared with sizeChoice |
| C10 | Every geometry field, not only height: padding, radius, and icon size are checked |
| C11 | CI regeneration drift gate: copied artifacts are byte-compared, never git diff |
| C12 | Native projection: the oracle validates the native token projection, not the web one |
Each check carries a fire test. A check without a recorded fire result is considered absent.
Frame Ownership
A field composite (password, search, number, date, combo box, select) has exactly one frame owner. The wrapper owns the border, the focus ring, the invalid state, and the disabled state; the inner TextInput renders unframed. Nested borders (a bordered input inside a bordered wrapper) are a defect, and so is a browser default focus outline: when the wrapper owns focus it renders the contract's focus treatment and suppresses the user agent outline.
The frame shape is a structure-tier token, feedback.field, with values underline (bottom border only, square corners) and outline (four sides). Components switch on the token; a brand layer changes the value. Every square corner reads shape.radius_00, so one layer override rounds fields, buttons, tiles, notifications, and menus together while pill shapes stay on shape.radius_max. If a component needs a code change to look right under a brand layer, the component hard-coded structure; fix the component, never widen the layer.
React Native Web renders TextInput as an <input> with an intrinsic minimum width. The atom sets minWidth: 0 so a field can shrink to its wrapper; composites do not patch this individually.
Icon Adapters Preserve Glyph Semantics
Components emit semantic icon names (close, chevron_down, visibility_off, trash) from a committed manifest, data/icon-names.json. Each host adapter maps every manifest name to one glyph in its icon set; the manifest carries one column per host and a test asserts every name has every column and every referenced export exists. Applications use the same semantic names; an icon-name literal in application source that is not a manifest key or alias fails a unit test.
An adapter renders the glyph as authored. Stroke glyphs depend on their own classes or attributes for fill: none and stroke width; an adapter that rewrites root styling with both fill and stroke turns open polylines into filled shapes. Icon size comes from the sheet's icon size tokens, never from a type set's font size.
An unmapped name renders a visible fallback and reports console.error. A silent fallback passes every zero-console-error gate while the page shows placeholders.
Status Surface Triad
Every status surface (inline, toast, actionable, static notification, callout, error state) uses the notification triad for a kind:
| Role | Token |
|---|---|
| Fill | color.notification_background_[kind] |
| Accent (leading border, icon) | color.support_[kind] |
| Text | color.text_primary, color.text_secondary |
| High contrast | color.background_inverse, color.text_inverse, color.support_[kind]_inverse |
support_[kind] is an accent color, never a fill; a support_* fill with dark text fails the contrast floor. Status surfaces read geometry and the radius token from the shared notification sheet.
Centered Targets
Every pressable meets a minimum target: field-adjacent controls (password toggle, clear, steppers, calendar) are controlSize square from the textInput sheet; notification dismiss is dismissTargetSize; every other pressable meets the shared target.minSize floor. A minimum size alone is not enough: the pressable centers its glyph (alignItems, justifyContent on a View or Pressable, never on the SVG itself, which rejects flex properties), so the visible glyph sits inside the hit region rather than in one corner. Every pressable also has an accessible name; a component that takes title and a caller that passes children (or the reverse) produces a nameless button, and the fidelity test rejects it.
Accessibility Contract
Components meet the accessibility contract through aria-* props, which React Native 0.71+ accepts as first-class aliases and React Native Web forwards to the DOM. State and value semantics are expressed through aria-* props, never through the deprecated accessibilityState or accessibilityValue props, which React Native Web does not forward to the DOM. accessibilityRole and accessibilityLabel remain correct and are used directly.
| Requirement | Implementation |
|---|---|
| Roles | accessibilityRole prop on every interactive component. Maps to ARIA role on web |
| Labels | accessibilityLabel on every component lacking a visible text label |
| State announcement | aria-checked, aria-expanded, aria-disabled, aria-selected, aria-invalid, aria-pressed, aria-current through the a11y translator |
| Value semantics | aria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext through the a11y translator |
| Relationships | aria-controls, aria-labelledby, aria-describedby, aria-owns, aria-activedescendant through the a11y translator |
| Focus management | Overlays that open/close (Modal, Dropdown, Popover) trap focus and restore on close |
| Hit target | Minimum 44x44 points on interactive components (iOS HIG), 48x48 dp (Android Material) |
| Focus indicator | Visible focus ring or outline in the focused state |
No-op props on web
The following React Native accessibility props are silent no-ops on web and must not be used. Use the aria-* equivalent instead.
| Prop or API | Web behavior | Use instead |
|---|---|---|
accessibilityState | Not forwarded to the DOM | aria-checked, aria-expanded, aria-disabled, etc. through a11y.state() |
accessibilityValue | Not forwarded to the DOM | aria-valuenow, aria-valuemin, aria-valuemax through a11y.value() |
accessibilityHint | Emits nothing | aria-describedby, pass both |
accessibilityElementsHidden, importantForAccessibility | Emit nothing | aria-hidden |
accessibilityViewIsModal | Emits nothing | aria-modal |
AccessibilityInfo.announceForAccessibility | Literal empty function | useAnnounce from LiveRegionProvider |
AccessibilityInfo.setAccessibilityFocus | No-op | DOM ref.focus() |
accessibilityActions / onAccessibilityAction | Unimplemented | onKeyDown on web |
LayoutAnimation | No-op | Animated with measured height |
Platform gaps
aria-* is the one form that works on web, iOS, and Android. However, native platforms have gaps: aria-live is Android-only on native, aria-modal is iOS-only, and native has no table, tabpanel, or landmark roles. The library routes every gap through a mechanism (such as useAnnounce for live regions) rather than leaving it to individual component judgment.
Generic vs Custom
The library ships generic atoms and molecules. Apps register their own variants and freeform components alongside the generic set.
Example: a restaurant suite has a POS application and a customer ordering application. Both share the same atom set (Text, Button, Icon, Image). The POS uses large-touch variant buttons (a structured variant with bigger hit targets and higher contrast). The ordering app uses the canonical button. Both variants read the same tokens; they differ at the molecule and variant layer.
App-registered variants live in the app's source, not in the library. The library provides the generic set and the extension points (Component.variant, Component.freeform). The app populates them.
What the Generic Component System Absorbs
The generic component system is designed to absorb any standard design system that can be expressed through the Superloom token contract. Two reference template packages prove this: Carbon (IBM) and Material (Google). Each template fills the same 379 contract tokens with its own values; the component library interprets those tokens into React Native API calls without knowing which design system provided them.
The generic system handles:
- Border radius, border thickness, and border color
- Background colors and surface layers
- Text colors, font families, font sizes, line heights, and weights
- Spacing scales for padding and margins
- Focus rings and interaction state colors
- Motion durations, easing curves (bezier), spring physics, and multi-segment curves
- Shadow elevation levels
- Layout dimensions (width, height, min/max constraints)
What the Generic Component System Cannot Absorb
The generic system cannot handle visual concepts that have no token representation:
- Custom artistic shapes (trapezoids, cloud-shaped CTAs, organic geometry) that require hardcoded SVG paths or custom drawing
- Visual elements that cannot be expressed through border radius, border thickness, or background color
- Animations with no contract token (e.g., a custom particle effect or a morphing shape)
- Platform-specific rendering that bypasses the React Native component model
A custom component system for these cases is a separate package. It declares its own required and supported token subset from the contract (or proposes contract additions for new concepts) and implements its own rendering and animation logic. The core Themer still validates whatever token subset that system declares as required.
Peer Dependencies
The component library declares its runtime dependencies as peer dependencies. The host app (the Expo project at src/client/) provides them.
| Dependency | Why it is a peer |
|---|---|
react | The host owns the React version |
react-native | The host owns the RN version (via Expo SDK) |
@expo/vector-icons | The host owns the icon set |
The library never bundles these. Test apps inside the library pin real versions as dev dependencies for isolation.
Further Reading
- Theming - The themer that produces the tokens components consume
- Client Loader - How the component loader enters the boot chain
- Client Architecture - Why the library targets React Native Web
- Client Modules - The naming taxonomy for the component library package