Skip to content

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

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.

TierDirectoryDefinitionBoundary
Atomatom/An irreducible primitive wrapping one RN element with token consumption and accessibility behaviorNo composition of other library components. No domain knowledge
Moleculemolecule/A composition of atoms with interaction logicNo domain knowledge. Composes atoms only; never composes other molecules
Compositecomposite/A composition of atoms, molecules, and other composites with coordination logicNo domain knowledge. May compose atoms, molecules, and other composites
Providerprovider/A context-only component that renders no visual output and consumes no tokensNo 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.

AtomRN elementKey props
ViewViewLayout, background, padding, margin
TextTextSize, color, weight, family
ButtonPressableVariant, size, state, onPress, accessibilityLabel
IconText (vector icon)Name, size, color
ImageImageSource, resize, aspect
TextInputTextInputValue, placeholder, state, accessibilityLabel
ToggleSwitchValue, onValueChange, state
TagViewLabel, color, dismissible
BadgeIndicatorViewCount, color, position
ProgressBarView (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.

MoleculeComposesInteraction
ButtonIcon + Text + PressableHover/press/disabled state resolution, icon + label layout, kind prop (primary, secondary, tertiary, danger, ghost)
DropdownButton + View + TextOpen/close state, selection, accessibility focus management
ModalView + Text + ButtonVisibility state, backdrop, focus trap
CardView + Text + ImageLayout, optional press state
ListItemView + Text + Icon + SeparatorSelection, 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.

CompositeComposesCoordination
MenuMenuItem, ViewContext for active item, roving tab index
TabsTab, TabList, TabPanelContext for active tab, aria-controls wiring
AccordionAccordionItemContext for expanded state
DataTableTable, TableRow, TableCellHeadless 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.

ProviderPurpose
OverlayOverlay stacking and z-index management
LiveRegionProviderScreen reader announcements through aria-live regions
LayerElevation level context for nested surfaces
ThemeTheme override context for subtrees
FeatureFlagsFeature flag context for conditional rendering
IdPrefixID prefix context for scoped id generation
FluidFormForm-level context for fluid label positioning
ErrorBoundaryError 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:

  1. createSystem generates CommonStyle (the utility style map for the current theme) and wires every component via make(factory)
  2. Each component factory is function (Lib, CONFIG, ERRORS, Parts, Registry, Style) returning a React component
  3. The component maps props to utility classes: size to font_size_[step], color to font_[token], weight to font_weight_[name]
  4. Molecules compose atoms through the shared Component object, not through direct imports
  5. Directional layout uses Parts.Direction, a context-based direction provider. Components that need the writing direction call Parts.Direction.useDirection(). Logical style properties (paddingStart, paddingEnd, marginStart, marginEnd) mirror automatically under RTL and require no manual intervention. Directional icons use Parts.Direction with a mirror prop and transform: [{ 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:

PropUtility classExample
sizefont_size_[step]size="md" to font_size_md
colorfont_[token]color="text_primary" to font_text_primary
weightfont_weight_[name]weight="semibold" to font_weight_semibold
backgroundbackground_[token]background="surface" to background_surface
paddingp_[side]_[step]padding="a_md" to p_a_md
marginm_[side]_[step]margin="t_lg" to m_t_lg
radiusbr_[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.

BucketLocationToken accessRegistryRe-themes
Canonicalatom/, molecule/, or composite/Full token access via CommonStyleComponent.[name]Yes
Providerprovider/No token access. Context onlyComponent.provider.[name]No
Structured variantvariant/Full token access via CommonStyleComponent.variant.[name]Yes
Unstructured freeformfreeform/No token access. No CommonStyle. Raw styles onlyComponent.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/ or freeform/
  • A provider component must register in Component.provider and render no visual output
  • A variant component must register in Component.variant
  • A freeform component must not receive CommonStyle or theme
  • 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.

StateMeaningVisual treatment
enabledDefault resting stateBase token values
hoveredPointer over the componentpseudoHover color operation (lightens dark colors, darkens light ones)
pressedComponent is being pressedpseudoPress color operation (stronger shift than hover)
focusedComponent has keyboard or screen-reader focusFocus ring or outline
disabledComponent is non-interactivedisabled color operation (45% original + 55% white)
loadingComponent is performing an async actionNon-interactive, announces aria-busy, renders a Loading or Skeleton
selectedComponent is the active choice in a groupAuthored selected token, not a pseudo-state derivation
checkedToggle/checkbox is onAuthored checked token
currentComponent marks the current page/stepAuthored current token
expandedComponent reveals additional contentaria-expanded semantics, visual indicator
invalidComponent has a validation errorAuthored 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:

CheckRule
C1Pin lock: the oracle records the source package version
C2No transcription without a reason: every transcribed entry carries a reason
C3Parse coverage floor: at least textInput, button, search, tag, select report parsed
C4Idempotency: regenerating the oracle produces a byte-identical artifact (minus timestamp)
C5Self-check constants: known shape and size values are asserted at generation time
C6Ledger completeness: every ledger component has a method
C7No orphans: a component in either oracle may not remain unspecced
C8Spec-versus-oracle agreement: spec sheet geometry matches the oracle for parsed entries
C9Cross-oracle declaration: disagreements between Carbon and Material are declared with sizeChoice
C10Every geometry field, not only height: padding, radius, and icon size are checked
C11CI regeneration drift gate: copied artifacts are byte-compared, never git diff
C12Native 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:

RoleToken
Fillcolor.notification_background_[kind]
Accent (leading border, icon)color.support_[kind]
Textcolor.text_primary, color.text_secondary
High contrastcolor.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.

RequirementImplementation
RolesaccessibilityRole prop on every interactive component. Maps to ARIA role on web
LabelsaccessibilityLabel on every component lacking a visible text label
State announcementaria-checked, aria-expanded, aria-disabled, aria-selected, aria-invalid, aria-pressed, aria-current through the a11y translator
Value semanticsaria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext through the a11y translator
Relationshipsaria-controls, aria-labelledby, aria-describedby, aria-owns, aria-activedescendant through the a11y translator
Focus managementOverlays that open/close (Modal, Dropdown, Popover) trap focus and restore on close
Hit targetMinimum 44x44 points on interactive components (iOS HIG), 48x48 dp (Android Material)
Focus indicatorVisible 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 APIWeb behaviorUse instead
accessibilityStateNot forwarded to the DOMaria-checked, aria-expanded, aria-disabled, etc. through a11y.state()
accessibilityValueNot forwarded to the DOMaria-valuenow, aria-valuemin, aria-valuemax through a11y.value()
accessibilityHintEmits nothingaria-describedby, pass both
accessibilityElementsHidden, importantForAccessibilityEmit nothingaria-hidden
accessibilityViewIsModalEmits nothingaria-modal
AccessibilityInfo.announceForAccessibilityLiteral empty functionuseAnnounce from LiveRegionProvider
AccessibilityInfo.setAccessibilityFocusNo-opDOM ref.focus()
accessibilityActions / onAccessibilityActionUnimplementedonKeyDown on web
LayoutAnimationNo-opAnimated 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.

DependencyWhy it is a peer
reactThe host owns the React version
react-nativeThe host owns the RN version (via Expo SDK)
@expo/vector-iconsThe 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

Released under the MIT License.