Theming
Theming for Ferrule, a React Native UI library for iOS, Android and web. Tokens, slots, nested themes and the rest of the one system.
All customization goes through one system. From broadest to narrowest:
| Level | How | Reaches |
|---|---|---|
| Palette | createTheme({ palette: 'cobalt' }), or <FerruleProvider palette="cobalt"> at runtime. Either takes your own too | Every color, in light and dark |
| Tokens and colors | createTheme({ tokens, colors }) | Every component |
| Component slots | createTheme({ components: { button: { label: {...} } } }) | One component everywhere |
| Variants and props | <Button variant="outline" size="lg" /> | One use |
style props | <Button style={...} labelStyle={...} /> | One use |
| The source | Edit components/ui/button.tsx. It is your code now. | Anything |
import { createTheme, FerruleProvider } from '@leoanimesh/ferrule';
const theme = createTheme({
palette: 'plum',
tokens: { radius: { md: 4 }, space: { 4: 18 } },
components: { card: { container: { borderWidth: 2 } } },
});
<FerruleProvider theme={theme} colorMode="system">...</FerruleProvider>;Unset values keep their defaults. Components render with the default light theme even with no provider, but Toast needs one to stay above overlays.
Colors
Each can be overridden per mode, on top of the palette, in colors.light and colors.dark:
background, surface, surfaceMuted, text, textMuted, border, controlBorder, primary/onPrimary, primarySoft/onPrimarySoft, the same four for danger, success and warning, overprint, focusRing, overlay.
overprint is laid over solid fills when pressed or hovered, chosen so that the label's contrast rises.
Tokens
| Token | Default |
|---|---|
space | 0:0 1:4 2:8 3:12 4:16 5:20 6:24 8:32 10:40 |
radius | none:0 sm:3 md:6 lg:10 full:9999 |
fontSize | xs:12 sm:14 md:16 lg:18 xl:22 2xl:28 3xl:36 4xl:56 |
lineHeight | xs:16 sm:20 md:24 lg:24 xl:28 2xl:34 3xl:42 4xl:64 |
fontWeight | regular:400 medium:500 semibold:600 bold:700 |
fontFamily | empty, meaning the system font |
controlHeight | sm:36 md:44 lg:52 |
minTouchTarget | 44 |
maxFontScale | 2 |
shadow | sm md lg, as boxShadow strings |
motion | fast:120 normal:240 (ms). The curve is EASE_OUT |
breakpoint | md:768 lg:1100 (window width in dp) |
Nested themes
A FerruleProvider inside another restyles only its subtree, which suits a section with its own brand. Its theme and palette are its own, while color mode, direction and safe-area insets carry over from the provider around it unless you set them. colorMode="system" still means the OS.
Responsive values
useBreakpoint() returns base, md or lg from the window width and the breakpoint tokens (768 and 1100 by default). useResponsive({ base: 1, md: 2, lg: 3 }) picks a value, and a missing band falls back to the next smaller one.
Gradients
<Gradient colors={[colors.primarySoft, colors.background]} angle={180}> draws a linear gradient behind its children using React Native's own backgroundImage, and mirrors its angle in right-to-left. Text on a gradient must pass contrast against every color in it, so build one from theme tokens.
Reading tokens in your own components
const { colors, tokens, mode } = useTheme();Animation
Components animate with Reanimated, re-exported from @leoanimesh/ferrule: Animated, useSharedValue, useAnimatedStyle, withTiming, withRepeat, cancelAnimation, interpolate, Easing, EASE_OUT, useReducedMotion, the entering and exiting builders FadeIn, FadeInDown, FadeInUp, FadeOut, FadeOutDown, FadeOutUp, ZoomIn and ZoomOut, plus StateLayer, Band and useOverlayMotion. withTiming follows the system reduced-motion setting by default, jumping to the end value. Effects cancel their animation on unmount.
Also available: useInsets(), useDirection(), useReducedMotion(), useSlot(component, slot), useControllableState(value, default, onChange), useRovingFocus(values, current, select, { upDown, disabled }) for arrow keys on web in a one-of-many control of your own.
Right-to-left
- iOS and Android: direction is process-wide. Call
I18nManager.forceRTL(true)and restart the app. - Web: pass
direction="rtl"toFerruleProvider. It applies live.
Components use logical styles, so nothing else is needed.
Safe area
React Native core has no safe-area API and Ferrule avoids a further native dependency, so useInsets() estimates from the screen size on iOS and from the status bar height on Android. Android's bottom inset estimates to 24 on API 35 and later, where apps draw edge to edge, and to 0 below it. Three-button navigation is taller, so wherever exact values matter, pass real ones. react-native-safe-area-context works, for example:
<FerruleProvider insets={useSafeAreaInsets()}>...</FerruleProvider>