Design Systems Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Start with tokens, not a library: Define color, spacing, and typography roles in a module before adopting NativeWind, Tamagui, or Paper - libraries consume tokens; they do not replace design decisions.
Name tokens by semantic role: Use surface, textPrimary, and accent instead of gray700 - palette overhauls and white-label flavors swap values without renaming every screen.
Keep spacing on a fixed scale: A 4px grid (4, 8, 16, 24) beats ad-hoc 13 and 17 padding - designers and engineers share one rhythm.
Expose small component APIs: Primitives accept variant, size, and disabled - not open-ended style props that recreate one-off buttons per feature.
One ThemeProvider at the root: Centralize scheme and brand selection - nested providers cause split-brain colors and double fetches on theme change.
Apply dynamic colors at render: Module-level StyleSheet.create with hard-coded hex does not react to dark mode - pair static layout with useTheme() or dark: utilities.
Pick one styling strategy per app: Mixing Tamagui screens, NativeWind screens, and raw StyleSheet without boundaries creates bundle bloat and inconsistent motion - choose a spine and wrap legacy incrementally.
Profile before adopting a compiler: NativeWind and Tamagui solve scale problems - a five-screen MVP does not need Metro transformers or tamagui.config.ts maintenance.
NativeWind: static class strings in lists: Dynamic `bg-${x}` breaks compile-time extraction - use fixed variant maps on row components.
Tamagui: typed variants over inline hex: styled() variants encode design states - $brand tokens keep dark mode and flavors coherent.
React Native Paper: always wrap PaperProvider: MD3 theme, ripple, and icons flow from context - screens that skip the provider silently fall back to defaults.
Paper + Navigation: use adaptNavigationTheme: Header and tab colors should match primary and surface - mismatched chrome reads as two different apps.
@expo/ui for native chrome, RN for density: Universal Host/Button/Switch excel at platform feel - virtualized lists and custom charts stay in RN (RNHostView islands).
SwiftUI modifiers are not RN styles: Widget and glass layouts use @expo/ui/swift-ui/modifiers - do not paste StyleSheet into extension targets.
Accessibility belongs in primitives: Set accessibilityRole="button" and labels on library Button/Pressable once - feature teams forget per-screen patches.
Support dynamic type on text primitives: Forward maxFontSizeMultiplier and test long German strings on Text wrappers - dense toolbars cap scaling; body copy should not.
Honor reduced motion in animated primitives: Loading skeletons and entrance transitions should respect system settings - gate Reanimated entering animations when reduced motion is on.
Contrast-test every brand flavor: White-label primary/onPrimary pairs must pass audit per tenant - one passing Acme palette does not certify Globex green.
Storybook documents states, not just happy path: Empty, loading, error, disabled, and long-text stories catch layout bugs before they reach production auth flows.
Keep Storybook out of production bundles: Use STORYBOOK_ENABLED entry swapping - never import .rnstorybook unconditionally in App.tsx.
Semver the UI package like any dependency: Renaming title → label is a major bump - mobile apps pin versions; document breaking visual changes in CHANGELOG.
Export a narrow public API: Barrel-export only primitives and hooks from index.ts - internal helpers prefixed or unexported prevent unsupported coupling.
White-label with flavor config, not git forks: BRAND_FLAVOR in EAS profiles + brands registry scales security patches - forks multiply SDK upgrade cost.
Isolate OTA channels per flavor: Publishing one client's JS bundle to another tenant's channel is a branding and data incident - map channels in eas.json.
Cross-review tokens in CI: Block new raw hex in feature folders and new inline styles in list renderItem - design system discipline erodes one PR at a time without lint gates.
When you have many screens sharing tokens and utility or variant ergonomics measurably beats StyleSheet maintenance. Stay on tokens + StyleSheet until copy-paste drift appears - see Design Systems Basics.
Choose Paper for MD3 Android-first products with standard forms and dialogs. Choose custom + tokens when brand language is not Material. See React Native Paper.
Drop-in native controls and hybrid layouts - not a full replacement for lists and navigation. See Expo UI Drop-Ins.
Flavor picks brand primary and assets; scheme picks neutral backgrounds and borders. Merge in one provider - see Theming & Brand Flavors.
Extract when two apps share primitives or when Storybook and semver matter. Single-app teams can use components/ until the second consumer appears - see Building an Internal Component Library.
accessibilityRole, visible label (or accessibilityLabel on icon-only controls), contrast on text, and VoiceOver/TalkBack smoke on each new primitive - see Accessibility Basics.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026