react-native-stylefn
v0.9.20
Published
A Tailwind-inspired styling library that injects design tokens into every React Native component's style props automatically
Maintainers
Readme
react-native-stylefn
A Tailwind-inspired styling library that injects design tokens into every React Native component's style prop — automatically, with zero per-component setup.
Write style functions instead of style objects. Every function receives rich contextual tokens — theme, dark mode, breakpoints, orientation, platform, safe area insets, and accessibility — and returns a standard RN style object. The Babel plugin resolves these functions at compile time so native components always receive plain style objects.
Features
- 🎨 Tailwind-inspired design tokens — spacing, fonts, colors, shadows, border radius, and more
- 🌓 Automatic dark mode — system-driven or manual toggle
- 📱 Responsive breakpoints —
sm,md,lg,xlderived from screen width - 🔄 Orientation-aware —
portrait/landscapetoken updates on rotation - 🛡️ Safe area insets — passed directly into every style function
- ♿ Accessibility tokens — reduced motion, font scale, bold text, high contrast
- ⚡ Zero runtime overhead — compile-time transform, no monkey-patching of React internals
- 🧩 Truly universal — works on ALL components (built-in, third-party, custom) — any prop ending in
styleorStyle - 🔮 Token functions in any prop — use
(t) => ...inwidth,height,columns, or any non-callback prop - 📦 Zero config — works out of the box with sensible defaults
- 🎯 Full TypeScript — automatic type patching, complete autocomplete on every token
- 🔌 Babel plugin — transforms style props at build time, patches
StyleSheet.createat runtime - 🎬 Reanimated compatible — works with
useAnimatedStyleviauseStyleFn()hook
Installation
npm install react-native-stylefn
# or
yarn add react-native-stylefnComplete Setup Guide
After installing, there are 6 steps to fully set up the library. Steps 1–3 are handled automatically by the postinstall script. Steps 4–6 require manual file edits. An optional Step 7 covers the design token config files.
TL;DR — Install, check that postinstall ran, add the Babel plugin, add the Metro wrapper, wrap your app in
<StyleProvider>, createglobal.cssif you want CSS variables. That's it.
Step 1 — Automatic: StyleProp<T> type patching (postinstall)
What happens: The postinstall script (scripts/setup.js) runs automatically after npm install / yarn add. It patches React Native's StyleProp<T> type definition so that every component's style prop accepts style functions.
Where it patches:
node_modules/react-native/
├── types_generated/Libraries/StyleSheet/StyleSheetTypes.d.ts ← StyleProp<T>
├── types_generated/Libraries/StyleSheet/StyleSheetExports.d.ts ← StyleSheet.create()
└── Libraries/StyleSheet/StyleSheet.d.ts ← (older RN versions)What the patch looks like:
// Before (React Native's original type):
export type StyleProp<T> = null | void | T | false | "" | ReadonlyArray<StyleProp<T>>;
// After (patched by react-native-stylefn):
export type StyleProp<T> = null | void | T | false | "" | ReadonlyArray<StyleProp<T>>
| ((tokens: import('react-native-stylefn').StyleTokens) => T | false | null | undefined);Since every React Native component (View, Text, ScrollView, FlatList, third-party components, etc.) uses StyleProp for its style props, this single patch makes style functions work everywhere.
It also patches StyleSheet.create() to accept style functions as values, so you can mix static styles and dynamic style functions in the same StyleSheet.create() call.
The script also creates a type stub at node_modules/react-native/node_modules/react-native-stylefn/ so that the import('react-native-stylefn').StyleTokens reference in the patched type always resolves correctly, regardless of monorepo layout.
If postinstall didn't run (e.g. you used
--ignore-scripts), run it manually:npx react-native-stylefn setup
After upgrading React Native, run it again — RN upgrades overwrite the type files:
npx react-native-stylefn setup
Step 2 — Automatic: tsconfig.json patching (postinstall)
What happens: The same postinstall script also patches your project's tsconfig.json to add jsxImportSource. This points TypeScript to a custom JSX runtime that makes ALL component props (not just style props) accept token functions.
What it adds to your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react-native-stylefn"
}
}Why this matters: Without this, only style props would accept (t) => ... functions (via the StyleProp patch from Step 1). With jsxImportSource, any non-callback prop like width, height, columns, color, etc. also accepts token functions — and TypeScript won't complain:
// Component declares plain types:
function Box({ width, color }: { width: number; color: string }) { ... }
// Consumer passes token functions — TypeScript is happy because jsxImportSource is set:
<Box
width={({ orientation }) => orientation.landscape ? 200 : 120}
color={({ dark }) => dark ? '#fff' : '#000'}
/>How it works: The custom JSX runtime lives in:
react-native-stylefn/
├── jsx-runtime/index.d.ts ← Production builds
└── jsx-dev-runtime/index.d.ts ← Development buildsThese files override TypeScript's LibraryManagedAttributes type to wrap every non-callback, non-style prop T with T | ((tokens: StyleTokens) => T). Props that are never widened (they keep their original types):
key,ref,children- Event handlers:
on*(onPress, onChange, etc.) - Render props:
render*,handle* - Style props:
style,*Style(already handled byStyleProppatching) - Known callbacks:
keyExtractor,getItem,ListHeaderComponent, etc.
Manual setup — If the postinstall script couldn't auto-patch your tsconfig (e.g. it has comments), add the settings manually:
// tsconfig.json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react-native-stylefn" } }
Step 3 — Automatic: stylefn.d.ts type declarations (generated by Metro)
What happens: When Metro starts (via withStyleFn() — set up in Step 5), it reads your rn-stylefn.config.js and global.css, then generates a stylefn.d.ts file that gives you full TypeScript autocomplete for all your theme keys.
Where it's generated:
your-project/
└── node_modules/react-native-stylefn/stylefn.d.ts ← auto-generatedA copy is also written to your project root as stylefn.d.ts if the library's installed location isn't writable.
What the generated file looks like:
// Auto-generated by react-native-stylefn — do not edit
export {};
declare module 'react-native-stylefn' {
interface ThemeKeyOverrides {
spacing: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '10' | '12';
fontSize: '2xl' | '3xl' | 'base' | 'lg' | 'sm' | 'xl' | 'xs';
borderRadius: '2xl' | 'full' | 'lg' | 'md' | 'none' | 'sm' | 'xl';
color: 'accent' | 'background' | 'border' | 'primary' | 'primary-foreground' | ...;
shadow: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'none' | ...;
breakpoint: 'lg' | 'md' | 'sm' | 'xl';
}
}How to reference it — Create a stylefn-env.d.ts file (or any .d.ts file) in your project's root that references the generated declarations:
// stylefn-env.d.ts
/// <reference types="react-native-stylefn/stylefn" />Make sure this file is included in your tsconfig.json's include pattern (e.g. "include": ["**/*.ts", "**/*.tsx"]).
Result: Your IDE now suggests all your actual theme keys everywhere:
t.theme.borderRadius['lg'] // ✅ autocomplete suggests 'sm', 'md', 'lg', 'xl', '2xl', 'full', 'none'
t.theme.spacing[4] // ✅ autocomplete suggests '0', '1', '2', '3', '4', '5', '6', '8', '10', '12'
t.colors.primary // ✅ autocomplete suggests 'primary', 'secondary', 'background', 'text', etc.
t.breakpoint.up('md') // ✅ autocomplete suggests 'sm', 'md', 'lg', 'xl'Note: This file is auto-generated every time Metro starts. Add
stylefn.d.tsto your.gitignore(the Metro wrapper does this automatically).
Step 4 — Manual: Add the Babel plugin
Create or update your babel.config.js to include the stylefn Babel plugin:
// babel.config.js
module.exports = {
presets: ['babel-preset-expo'], // or your existing preset
plugins: ['react-native-stylefn/babel-plugin'],
};What the Babel plugin does (at compile time):
Transforms style props — wraps function/array expressions in
__resolveStyle():style={(t) => ({...})} → style={__resolveStyle((t) => ({...}))}Transforms non-style token props — wraps arrow functions in non-callback props with
__resolveProp():width={({ orientation }) => ...} → width={__resolveProp(({ orientation }) => ...)}Auto-imports
react-native-stylefn/auto— injects a side-effect import that patchesStyleSheet.createto support style functions at runtime.
Important: Clear your Metro cache after adding/changing the Babel config:
npx expo start --clear # or npx react-native start --reset-cache
Step 5 — Manual: Add the Metro config wrapper
Update your metro.config.js to wrap with withStyleFn:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withStyleFn } = require('react-native-stylefn/metro-config');
const config = getDefaultConfig(__dirname);
module.exports = withStyleFn(config, {
input: './global.css', // path to your CSS variables file (optional)
config: './rn-stylefn.config.js', // path to your config file (optional, this is the default)
});What withStyleFn() does:
- Parses
global.css— reads CSS custom properties from:rootand.darkselectors - Creates a virtual module — writes parsed CSS variables to
node_modules/react-native-stylefn/css-vars.jsand registers a Metro resolver soreact-native-stylefn/css-varsresolves to it - Resolves the config file — maps
rn-stylefn.configimports to your config file - Generates
stylefn.d.ts— type declarations for autocomplete (Step 3 above) - Updates
.gitignore— addsstylefn.d.tsto your.gitignore
Step 6 — Manual: Wrap your app with StyleProvider
In your root layout or App component, wrap everything with StyleProvider:
import { StyleProvider } from 'react-native-stylefn';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function App() {
const insets = useSafeAreaInsets();
// rn-stylefn.config.js and global.css are auto-loaded — no imports needed
return (
<StyleProvider insets={insets}>
<RootNavigator />
</StyleProvider>
);
}StyleProvider subscribes to device state (dimensions, color scheme, accessibility) and updates the token store. Children always render with current token values.
Note:
insetsare optional but recommended. If you don't usereact-native-safe-area-context, you can omit theinsetsprop.
Step 7 — Optional: Create design token files
global.css — CSS custom properties
Create a global.css in your project root to define light/dark color palettes and design tokens:
/* global.css */
:root {
/* --color-* prefix → available as t.colors.* */
--color-background: #ffffff;
--color-text: #111827;
--color-primary: #3b82f6;
/* Generic variables → available for var() resolution in config */
--primary: 224 71% 51%;
--primary-foreground: 210 20% 98%;
--radius: 8;
/* Shadow levels → available for var() resolution in config */
--shadow-1: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
}
.dark {
--color-background: #0f172a;
--color-text: #f8fafc;
--color-primary: #60a5fa;
--primary: 216 91% 70%;
--primary-foreground: 221 39% 11%;
--radius: 8;
--shadow-1: 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
}rn-stylefn.config.js — Theme configuration
Create a rn-stylefn.config.js in your project root to customize design tokens:
// rn-stylefn.config.js
module.exports = {
darkMode: 'system', // 'system' | 'manual'
theme: {
spacing: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 8: 32 },
fontSize: { xs: 10, sm: 12, base: 14, lg: 16, xl: 20, '2xl': 24 },
borderRadius: {
sm: 'calc(var(--radius) - 4px)', // CSS expression → resolved from global.css
md: 'calc(var(--radius) - 2px)',
lg: 'var(--radius)',
full: 9999,
},
colors: {
primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
danger: '#ef4444',
},
screens: { sm: 0, md: 375, lg: 430, xl: 768 },
},
};Both files are auto-loaded — no manual imports required. Just create them and restart Metro.
Setup Checklist
Here's a summary checklist for verifying your setup:
| # | What | File | Auto / Manual |
|---|------|------|---------------|
| 1 | StyleProp<T> patching | node_modules/react-native/.../StyleSheetTypes.d.ts | ✅ Auto (postinstall) |
| 2 | tsconfig.json — jsx + jsxImportSource | tsconfig.json | ✅ Auto (postinstall) |
| 3 | stylefn.d.ts — theme key autocomplete | node_modules/react-native-stylefn/stylefn.d.ts | ✅ Auto (Metro start) |
| 4 | Babel plugin | babel.config.js | ✍️ Manual |
| 5 | Metro config wrapper | metro.config.js | ✍️ Manual |
| 6 | StyleProvider in root component | app/_layout.tsx or App.tsx | ✍️ Manual |
| 7a | global.css (CSS variables) | global.css | ✍️ Optional |
| 7b | rn-stylefn.config.js (theme config) | rn-stylefn.config.js | ✍️ Optional |
| 7c | stylefn-env.d.ts (reference generated types) | stylefn-env.d.ts | ✍️ Optional |
Troubleshooting Setup
| Problem | Solution |
|---------|----------|
| StyleProp doesn't accept functions | Run npx react-native-stylefn setup manually |
| No autocomplete on t.theme.* | Check that stylefn-env.d.ts references the generated types, and restart your TS server |
| jsxImportSource not in tsconfig | Add "jsx": "react-jsx" and "jsxImportSource": "react-native-stylefn" to compilerOptions |
| Metro can't find css-vars module | Make sure withStyleFn() wraps your Metro config in metro.config.js |
| Style functions not being resolved | Check that react-native-stylefn/babel-plugin is in your babel.config.js plugins |
| Types broken after RN upgrade | Run npx react-native-stylefn setup — RN upgrades overwrite the patched type files |
| Cache issues after config changes | Run npx expo start --clear or npx react-native start --reset-cache |
Usage Examples
Once setup is complete, you can start using style functions immediately.
1. Write style functions
import { View, Text, ScrollView } from 'react-native';
// No special imports — just write a function where you'd write a style object
<View style={(t) => ({
backgroundColor: t.dark ? t.colors.background : t.colors.surface,
padding: t.breakpoint.up('lg') ? 24 : 12,
flexDirection: t.orientation.landscape ? 'row' : 'column',
})} />
<Text style={(t) => ({
fontSize: t.theme.fontSize.base,
color: t.theme.colors.primary,
fontWeight: t.theme.fontWeight.bold,
})} />
<ScrollView
contentContainerStyle={(t) => ({
padding: t.theme.spacing[4],
paddingTop: t.insets.top + t.theme.spacing[4],
paddingBottom: t.insets.bottom + t.theme.spacing[6],
})}
/>2. Mix functions and objects in arrays
<View style={[
{ flex: 1 },
(t) => ({ backgroundColor: t.dark ? '#000' : '#fff' }),
(t) => t.breakpoint.down('md') && { padding: 8 },
]} />3. Use with StyleSheet.create
import { StyleSheet, View, Text } from 'react-native';
const styles = StyleSheet.create({
// Dynamic — resolved at render time
container: (t) => ({
flex: 1,
backgroundColor: t.colors.background,
padding: t.theme.spacing[4],
}),
// Static — processed normally
badge: {
paddingHorizontal: 8,
borderRadius: 4,
backgroundColor: '#3b82f6',
},
});
<View style={styles.container}>
<View style={styles.badge} />
</View>4. Use with Reanimated
import Animated, { useAnimatedStyle, withSpring, useSharedValue } from 'react-native-reanimated';
import { useStyleFn } from 'react-native-stylefn';
function AnimatedCard() {
const { colors, theme, dark } = useStyleFn();
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Pressable onPress={() => { scale.value = withSpring(0.95); }}>
<Animated.View style={[
{
backgroundColor: dark ? '#1e293b' : colors.surface,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing[4],
},
animatedStyle,
]} />
</Pressable>
);
}5. Build custom components with style functions
import { useStyleFn } from 'react-native-stylefn';
function StyledCard({ style, children }) {
const tokens = useStyleFn();
// Resolve: if style is a function, call it with tokens
const resolvedStyle = typeof style === 'function' ? style(tokens) : style;
return <View style={resolvedStyle}>{children}</View>;
}
// Usage
<StyledCard style={(t) => ({
backgroundColor: t.colors.surface,
padding: t.theme.spacing[4],
borderRadius: t.theme.borderRadius.lg,
})}>
<Text>Custom component with style functions</Text>
</StyledCard>6. Token functions in ANY prop (not just style)
Token functions aren't limited to style props — you can use them in any prop that accepts a value. The Babel plugin automatically detects arrow functions in non-callback props and resolves them with the current tokens.
No special types needed! Your component can declare plain types like width: number and consumers can still pass token functions. The custom JSX runtime handles TypeScript automatically.
// Component uses plain types — no PropFunction<T> needed:
function ResponsiveBox({ width, height, color }: { width: number; height: number; color: string }) {
return <View style={{ width, height, backgroundColor: color }} />;
}
// ✅ Consumers pass token functions — TypeScript is happy, Babel resolves at runtime:
<ResponsiveBox
width={({ orientation }) => orientation.landscape ? 200 : 120}
height={80}
color={({ dark }) => dark ? '#3b82f6' : '#2563eb'}
/>// ✅ These all work automatically — the Babel plugin wraps them for you
<StrokePreview
width={({ orientation }) => orientation.landscape ? 266 : 200}
height={180}
isEraser={disabled}
/>
<Image
source={require('./avatar.png')}
width={({ breakpoint }) => breakpoint.up('lg') ? 120 : 80}
height={({ breakpoint }) => breakpoint.up('lg') ? 120 : 80}
borderRadius={({ breakpoint }) => breakpoint.up('lg') ? 60 : 40}
/>
<Grid
columns={({ breakpoint }) => breakpoint.up('xl') ? 4 : breakpoint.up('md') ? 2 : 1}
spacing={({ theme }) => theme.spacing[4]}
/>
<Canvas
backgroundColor={({ dark }) => dark ? '#1a1a2e' : '#ffffff'}
lineWidth={({ platform }) => platform.web ? 2 : 1}
/>Smart detection: The plugin only wraps arrow/function expressions. Event handlers (
onPress,onChange, etc.), render props (renderItem,renderHeader, etc.), and known callbacks (keyExtractor,ref, etc.) are never wrapped — they keep working as normal callbacks.
How it works under the hood
Compile Time (Babel) Runtime
──────────────────── ───────
width={({ orientation }) => ...} __resolveProp calls
│ the function with
▼ current tokens and
width={__resolveProp(({ orientation }) => ...)} returns the value7. usePropsFn hook — resolve multiple token props at once
For cases where you need explicit control, or when building wrapper components that pass resolved values to third-party libraries, use the usePropsFn hook:
import { usePropsFn } from 'react-native-stylefn';
function StrokePreview({ brushState, isEraser }) {
const { width, height, columns } = usePropsFn({
width: ({ orientation }) => orientation.landscape ? 266 : 200,
height: 180, // static values pass through unchanged
columns: ({ breakpoint }) => breakpoint.up('lg') ? 3 : 2,
});
return <Canvas width={width} height={height} columns={columns} />;
}// Great for third-party components that don't go through the Babel plugin
function ResponsiveSlider() {
const { sliderWidth, thumbSize, trackHeight } = usePropsFn({
sliderWidth: ({ screen }) => screen.width - 32,
thumbSize: ({ breakpoint }) => breakpoint.up('md') ? 24 : 16,
trackHeight: ({ breakpoint }) => breakpoint.up('md') ? 6 : 4,
});
return (
<Slider
style={{ width: sliderWidth }}
thumbSize={thumbSize}
trackHeight={trackHeight}
/>
);
}// Use with color picker, drawing tools, or any responsive component
function DrawingToolbar({ brushState }) {
const { panelWidth, swatchSize, previewSize } = usePropsFn({
panelWidth: ({ orientation }) => orientation.landscape ? 360 : 280,
swatchSize: ({ breakpoint }) => breakpoint.up('lg') ? 32 : 26,
previewSize: ({ orientation, breakpoint }) => ({
width: orientation.landscape ? 266 : 200,
height: breakpoint.up('lg') ? 200 : 180,
}),
});
return (
<View style={{ width: panelWidth }}>
<Swatches swatchStyle={{ width: swatchSize, height: swatchSize }} />
<StrokePreview width={previewSize.width} height={previewSize.height} />
</View>
);
}8. PropFunction<T> (optional — for explicit typing)
With jsxImportSource configured (done automatically by postinstall), you don't need PropFunction<T> — plain types like width: number already accept token functions in JSX.
However, if you prefer explicit typing or need it for non-JSX contexts, PropFunction<T> is still available:
import type { PropFunction } from 'react-native-stylefn';
// Option A: Plain types (recommended — works automatically with jsxImportSource)
interface BoxProps {
width: number;
height: number;
}
// Option B: Explicit PropFunction (still works, useful for documentation)
interface BoxProps {
width: PropFunction<number>;
height: PropFunction<number>;
}
// Both allow consumers to pass:
<Box width={266} height={180} /> // static
<Box width={({ orientation }) => orientation.landscape ? 266 : 200} height={180} /> // dynamicHow Types Work — Universal & Automatic
The type system works through two complementary mechanisms, both set up automatically by the postinstall script:
1. Style Props — StyleProp<T> patching
The postinstall script patches React Native's StyleProp<T> type definition to include style functions:
// Before (RN's original type):
type StyleProp<T> = null | void | T | false | "" | ReadonlyArray<StyleProp<T>>;
// After (patched by react-native-stylefn):
type StyleProp<T> = null | void | T | false | "" | ReadonlyArray<StyleProp<T>>
| ((tokens: StyleTokens) => T | false | null | undefined);Since every React Native component uses StyleProp for its style props, this single patch makes style functions work everywhere — View, Text, ScrollView, FlatList, third-party components, custom components — any prop typed as StyleProp.
2. Non-Style Props — Custom JSX Runtime (jsxImportSource)
The postinstall script also adds jsxImportSource: "react-native-stylefn" to your tsconfig.json. This points TypeScript to a custom JSX runtime that overrides LibraryManagedAttributes — the type TypeScript uses to check JSX props.
The custom runtime wraps every non-callback, non-style prop type T with T | ((tokens: StyleTokens) => T), so any component prop automatically accepts token functions without needing PropFunction<T>:
// Your component — plain types, nothing special:
function Box({ width, color }: { width: number; color: string }) { ... }
// TypeScript sees the JSX props as:
// width: number | ((tokens: StyleTokens) => number)
// color: string | ((tokens: StyleTokens) => string)
// So this just works:
<Box width={({ breakpoint }) => breakpoint.up('lg') ? 200 : 120} color="#fff" />Props that are never widened (they keep their original types):
key,ref,children- Event handlers:
on*(onPress, onChange, etc.) - Render props:
render*,handle* - Style props:
style,*Style(already handled by StyleProp patching) - Known callbacks:
keyExtractor,getItem,ListHeaderComponent, etc.
Manual setup
You can also run the setup manually:
npx react-native-stylefn setupOr add the tsconfig settings yourself:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react-native-stylefn"
}
}Token Reference
Every style function receives a StyleTokens object:
| Token | Type | Description |
|-------|------|-------------|
| theme | object | Full resolved theme (spacing, fontSize, borderRadius, fontWeight, colors, shadows, opacity) |
| colors | Record<string, string> | Resolved color palette for the current color scheme |
| dark | boolean | Whether dark mode is active |
| colorScheme | 'light' \| 'dark' | Current color scheme |
| breakpoint | BreakpointQuery | Breakpoint queries: .current, .up(name), .down(name) |
| screen | { width, height, scale, fontScale } | Screen dimensions |
| orientation | OrientationTokens | Boolean flags: .landscape, .portrait |
| platform | PlatformTokens | Boolean flags: .ios, .android, .web, .windows, .macos |
| insets | { top, bottom, left, right } | Safe area insets |
| reducedMotion | boolean | User prefers reduced motion |
| fontScale | number | Current font scale multiplier |
| boldText | boolean | Bold text enabled (iOS) |
| highContrast | boolean | High contrast enabled |
Breakpoint Queries
// t.breakpoint.current → 'sm' | 'md' | 'lg' | 'xl' (active breakpoint name)
// t.breakpoint.up('md') → true when screen width >= md threshold (375dp)
// t.breakpoint.down('lg') → true when screen width < lg threshold (430dp)
<View style={(t) => ({
padding: t.breakpoint.up('lg') ? 24 : 12,
flexDirection: t.breakpoint.up('xl') ? 'row' : 'column',
})} />Orientation Booleans
// t.orientation.landscape → true when width >= height
// t.orientation.portrait → true when height > width
<View style={(t) => ({
flexDirection: t.orientation.landscape ? 'row' : 'column',
})} />Platform Booleans
// t.platform.ios → true on iOS
// t.platform.android → true on Android
// t.platform.web → true on Web
// t.platform.windows → true on Windows
// t.platform.macos → true on macOS
<View style={(t) => ({
paddingTop: t.platform.ios ? 44 : 0,
fontFamily: t.platform.ios ? 'SF Pro' : 'Roboto',
})} />Shadow Tokens
Shadows use the boxShadow CSS string format (supported in React Native 0.76+):
<View style={(t) => ({
...t.theme.shadows.md,
// Expands to: { boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), ...' }
})} />Hooks
useStyleFn()
Access tokens inside component logic, event handlers, or animations:
import { useStyleFn } from 'react-native-stylefn';
function MyComponent() {
const { dark, colors, breakpoint, theme } = useStyleFn();
return (
<Pressable onPress={() => analytics.track('tap', { theme: dark ? 'dark' : 'light' })}>
<Text style={{ color: colors.text, fontSize: theme.fontSize.base }}>Hello</Text>
</Pressable>
);
}useTheme()
Manual dark mode control (when darkMode: 'manual' is set):
import { useTheme } from 'react-native-stylefn';
function SettingsScreen() {
const { theme, setTheme, toggleTheme } = useTheme();
return (
<View>
<Text>Dark Mode: {theme ? 'On' : 'Off'}</Text>
<Switch value={theme} onValueChange={toggleTheme} />
</View>
);
}usePropsFn()
Resolve an object of props where any value can be a token function. Re-renders when tokens change (orientation, breakpoint, dark mode, etc.):
import { usePropsFn } from 'react-native-stylefn';
function ResponsivePanel() {
const { width, height, columns } = usePropsFn({
width: ({ orientation }) => orientation.landscape ? 360 : 280,
height: 180, // static values pass through unchanged
columns: ({ breakpoint }) => breakpoint.up('lg') ? 3 : 2,
});
return <Grid width={width} height={height} columns={columns} />;
}This is especially useful for passing resolved values to third-party components that don't go through the Babel plugin.
Configuration
StyleProvider Props
<StyleProvider
config={{ darkMode: 'system', theme: { /* overrides */ } }}
insets={safeAreaInsets}
cssVars={parsedCSSVariables}
>
{children}
</StyleProvider>Using rn-stylefn.config.js
Create a config file at the root of your project:
// rn-stylefn.config.js
module.exports = {
darkMode: 'system', // 'system' | 'manual'
theme: {
// Override built-in spacing scale
spacing: {
0: 0, 1: 4, 2: 8, 3: 12, 4: 16,
5: 20, 6: 24, 8: 32, 10: 40, 12: 48,
},
// Override font sizes
fontSize: {
xs: 10, sm: 12, base: 14, lg: 16,
xl: 20, '2xl': 24, '3xl': 30,
},
// Override border radii
borderRadius: {
none: 0, sm: 4, md: 8, lg: 12,
xl: 16, '2xl': 24, full: 9999,
},
// Override font weights
fontWeight: {
normal: '400', medium: '500', semibold: '600', bold: '700',
},
// Override breakpoints (screen widths in dp)
screens: { sm: 0, md: 375, lg: 430, xl: 768 },
// Add your brand colors (available via t.theme.colors.*)
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6',
danger: '#ef4444',
success: '#22c55e',
warning: '#f59e0b',
},
// extend: merges on top of the built-in theme without replacing
extend: {
colors: { brand: '#ff6600', 'brand-dark': '#cc5200' },
spacing: { 13: 52, 14: 56 },
},
},
};Auto-loaded:
StyleProviderautomaticallyrequiresrn-stylefn.config.jsfrom your project root — no manual import or prop needed. Just create the file and your tokens are available everywhere.
CSS Variable Expressions in Config
Theme values in rn-stylefn.config.js can reference CSS variables from global.css using standard CSS expression syntax. This enables a Tailwind CSS-style config where colors, sizes, and shadows are driven by CSS custom properties that change between light/dark mode:
// rn-stylefn.config.js
module.exports = {
theme: {
// var() — resolves CSS variable, then parses as number
borderRadius: {
sm: 'calc(var(--radius) - 4px)', // → 4
md: 'calc(var(--radius) - 2px)', // → 6
lg: 'var(--radius)', // → 8
},
// hsl(var()) — resolves CSS variable, then converts HSL to hex
colors: {
border: 'hsl(var(--border))', // → '#e8e9eb'
primary: {
DEFAULT: 'hsl(var(--primary))', // → '#2662d9'
foreground: 'hsl(var(--primary-foreground))', // → '#f5f7fa'
},
},
// var() for shadows — resolves to boxShadow CSS string
// boxShadow is a Tailwind-compatible alias for shadows
boxShadow: {
sm: 'var(--shadow-1)', // → { boxShadow: '0px 1px 2px ...' }
md: 'var(--shadow-4)',
lg: 'var(--shadow-8)',
},
},
};Supported CSS expression syntax:
| Expression | Example | Resolves to |
|------------|---------|-------------|
| var(--name) | 'var(--radius)' | Value from CSS variables |
| var(--name, fallback) | 'var(--radius, 8)' | Value with fallback |
| hsl(...) | 'hsl(220 13% 91%)' | Hex color string |
| hsl(var(--name)) | 'hsl(var(--primary))' | HSL from CSS var → hex |
| calc(...) | 'calc(var(--radius) - 2px)' | Evaluated number |
| rgb(...)/rgba(...) | 'rgb(59 130 246)' | Hex color string |
Nested color objects (Tailwind convention) are automatically flattened:
primary: {
DEFAULT: 'hsl(var(--primary))', // → t.theme.colors.primary
foreground: 'hsl(var(--primary-foreground))', // → t.theme.colors['primary-foreground']
}Color-scheme aware: CSS variable expressions are resolved per color scheme — light mode uses
:rootvariables, dark mode uses.darkvariables. Sohsl(var(--primary))resolves to different colors in light vs dark mode.
If you need to pass config programmatically (e.g. from a remote source), you can still use the config prop:
<StyleProvider config={myDynamicConfig} insets={insets}>Now your custom tokens are available in every style function:
<Text style={(t) => ({
color: t.theme.colors.brand, // '#ff6600'
fontSize: t.theme.fontSize['3xl'], // 30
padding: t.theme.spacing[8], // 32
})} />Using global.css
Create a global.css in your project root to define your light/dark color palette and design tokens. Two naming conventions are supported:
/* global.css */
:root {
/* ---- Color palette (--color-* → t.colors.*) ---- */
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-border: #e2e8f0;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
/* ---- Generic variables (for var() resolution in config) ---- */
/* HSL color values — use with hsl(var(--primary)) in config */
--border: 220 13% 91%;
--primary: 224 71% 51%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14% 96%;
--secondary-foreground: 221 39% 11%;
--destructive: 0 84% 60%;
--muted: 220 14% 96%;
--muted-foreground: 220 9% 46%;
/* Design tokens — use with var(--radius), calc(var(--radius) - 2px) */
--radius: 8;
/* Shadow levels — use with var(--shadow-N) in config */
--shadow-0: none;
--shadow-1: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
--shadow-4: 0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.1);
}
.dark {
/* Dark mode colors */
--color-background: #0f172a;
--color-surface: #1e293b;
--color-border: #334155;
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--color-primary: #60a5fa;
--color-secondary: #a78bfa;
/* Dark mode HSL overrides */
--border: 215 28% 17%;
--primary: 216 91% 70%;
--primary-foreground: 221 39% 11%;
--secondary: 215 28% 17%;
--radius: 8;
--shadow-1: 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
--shadow-4: 0px 4px 6px -1px rgba(0, 0, 0, 0.4), 0px 2px 4px -2px rgba(0, 0, 0, 0.3);
}Variable naming:
--color-<name>format: The--color-prefix is stripped, so--color-text→t.colors.text--<name>format: Available forvar(--<name>)resolution in config values. Both are stored inrawVarsfor CSS expression resolution.
Auto-loaded: When you set
input: './global.css'inwithStyleFn()(step 2 of Quick Start), Metro processes the CSS at build time andStyleProviderloads it automatically as a virtual module — no manual import or prop needed.
If you need to pass CSS variables programmatically (e.g. without Metro or from a dynamic source), you can still use the cssVars prop with parseCSSVariables:
import { StyleProvider, parseCSSVariables } from 'react-native-stylefn';
import cssRaw from '../global.css'; // requires "*.css" in metro sourceExts
const cssVars = parseCSSVariables(cssRaw); // pre-parse once, outside component
export default function RootLayout() {
const insets = useSafeAreaInsets();
return (
<StyleProvider cssVars={cssVars} insets={insets}>
{/* your app */}
</StyleProvider>
);
}Now use the colors in style functions:
<View style={(t) => ({
backgroundColor: t.colors.background, // switches with dark mode
borderColor: t.colors.border,
})} />
<Text style={(t) => ({
color: t.colors.text,
fontSize: t.theme.fontSize.base,
})} />Using both config and CSS variables together:
Both are auto-loaded — no manual imports or props required. Just:
- Create
rn-stylefn.config.jsat your project root - Set
input: './global.css'inwithStyleFn()inmetro.config.js - Wrap your app with
<StyleProvider insets={insets}>
StyleProvider automatically picks up both at runtime. Your theme tokens and CSS color variables are all available in every style function.
How It Works
The library uses a compile-time Babel transform — no monkey-patching of React.createElement or jsx/jsxs:
Compile Time (Babel) Runtime
──────────────────── ───────
style={(t) => ({...})} __resolveStyle calls
│ the function with
▼ current tokens from
style={__resolveStyle((t) => ({...}))} the singleton storeThe Babel plugin does three things:
Transforms JSX style props — wraps function/array/variable expressions in
__resolveStyle()at compile time. Plain object literals (style={{ padding: 10 }}) are left untouched.Transforms non-style token props — detects arrow/function expressions in any non-callback prop (e.g.
width,height,columns) and wraps them in__resolveProp(). Event handlers (on*), render props (render*), and known callbacks (keyExtractor,ref, etc.) are never touched.Injects
import 'react-native-stylefn/auto'— patchesStyleSheet.createso it accepts style functions alongside static styles.
At runtime:
__resolveStyle(value)— if the value is a function, calls it with the current token store and resolves viewport units; if it's an array, maps over it resolving any functions; otherwise returns as-is.__resolveProp(value)— if the value is a function, calls it with the current token store and returns the raw result (no viewport unit conversion). Used for non-style props likewidth,height,columns.StyleProvider— subscribes to device state (dimensions, color scheme, accessibility) and updates the token store synchronously viauseMemoso children always render with current values.- Token store — a synchronous singleton, always readable from anywhere.
Why not monkey-patch React.createElement?
Patching React.createElement or jsx/jsxs is fragile — it breaks with React Native's new architecture (Fabric/JSI), doesn't work with virtual modules (Expo polyfills), and creates circular dependency issues in monorepos. The compile-time Babel transform is simpler, more reliable, and has zero runtime overhead for non-function styles.
Architecture
src/
├── resolve.ts # __resolveStyle + __resolveProp — resolve style/prop functions
├── patch.ts # patches StyleSheet.create only
├── auto.ts # side-effect import that calls applyPatch()
├── store.ts # singleton token store (get/set/subscribe)
├── provider.tsx # StyleProvider — syncs device state into token store
├── create.ts # StyleSheet.create replacement supporting style functions
├── config/
│ ├── defaults.ts # built-in Tailwind-inspired theme defaults
│ ├── loader.ts # config loading utility
│ ├── resolver.ts # deep merge: defaults + user config + extend
│ └── cssParser.ts # parses global.css variables
├── tokens/
│ ├── breakpoint.ts # breakpoint queries (up/down) from screen width
│ ├── orientation.ts # portrait/landscape boolean flags
│ ├── platform.ts # ios/android/web/windows/macos boolean flags
│ ├── accessibility.ts
│ └── index.ts # assembles full StyleTokens
├── hooks/
│ ├── useStyleFn.ts # access tokens in component logic
│ ├── useTheme.ts # manual dark mode toggle
│ └── usePropsFn.ts # resolve token functions in any prop
├── types.ts # full TypeScript types (StyleTokens, PropFunction, etc.)
├── stylefn.d.ts # type augmentation for RN components
└── index.tsx # public exports
jsx-runtime/
├── index.js # re-exports react/jsx-runtime
└── index.d.ts # custom JSX types — widens all props to accept token fns
jsx-dev-runtime/
├── index.js # re-exports react/jsx-dev-runtime
└── index.d.ts # same as jsx-runtime (for development builds)
babel-plugin/
└── index.js # compile-time transform (style + prop + auto-import)
scripts/
└── setup.js # postinstall: patches RN's StyleProp type + tsconfigAPI Reference
Exports from react-native-stylefn
| Export | Description |
|--------|-------------|
| StyleProvider | Provider component — wraps your app |
| useStyleFn() | Access tokens in component logic |
| useTheme() | Manual dark mode control |
| usePropsFn() | Resolve token functions in any prop (hook) |
| create() | StyleSheet.create replacement with style function support |
| __resolveStyle() | Style resolver for style props (used by Babel plugin) |
| __resolveProp() | Prop resolver for non-style props (used by Babel plugin) |
| getTokenStore() | Direct access to the token store singleton |
| applyPatch() | Manually apply the StyleSheet.create patch |
| resolveConfig() | Resolve user config with defaults |
| parseCSSVariables() | Parse CSS variable file content |
| defaultTheme | Built-in theme defaults |
| defaultConfig | Built-in config defaults |
| defaultCSSVariables | Built-in CSS variable defaults |
Types
| Type | Description |
|------|-------------|
| StyleTokens | Full token store shape passed to every token function |
| StyleFunction<S> | Style function type: (tokens: StyleTokens) => S |
| StyleProp<S> | Style prop: static, function, or array of both |
| PropFunction<T> | A prop value that can be static or a token function: T \| (tokens: StyleTokens) => T |
| TokenProp<T> | Alias for PropFunction<T> (from usePropsFn) |
| ThemeKeyRegistry | Known theme key registry (extensible via module augmentation) |
CSS Expression Utilities
| Export | Description |
|--------|-------------|
| resolveColorExpression() | Resolve var(), hsl(), rgb() in a color string |
| resolveNumericExpression() | Resolve var(), calc() in a numeric string |
| resolveShadowExpression() | Resolve var() in a shadow string |
| resolveCssExpression() | Auto-detect and resolve any CSS expression |
| flattenColors() | Flatten nested Tailwind-style color objects |
| getRawVarsForScheme() | Get raw CSS vars map for a color scheme |
TypeScript Autocomplete for Theme Keys
All theme properties provide autocomplete for known keys out of the box:
// ✅ Full autocomplete — IDE suggests 'sm', 'md', 'lg', 'xl', '2xl', 'full', 'none'
t.theme.borderRadius['lg']
// ✅ Full autocomplete — IDE suggests '0', '1', '2', '3', '4', '5', '6', '8', '10', '12'
t.theme.spacing[4]
// ✅ Full autocomplete — IDE suggests 'primary', 'secondary', 'background', 'text', etc.
t.colors.primary
// ✅ Full autocomplete — IDE suggests 'sm', 'md', 'lg', 'xl'
t.breakpoint.up('md')Custom keys from your config also work — they just won't appear in autocomplete unless you extend ThemeKeyRegistry.
Extending with Module Augmentation
To add autocomplete for your own custom keys, extend ThemeKeyRegistry via module augmentation:
// stylefn-env.d.ts (or any .d.ts file in your project)
declare module 'react-native-stylefn' {
interface ThemeKeyRegistry {
// Add your custom color keys
color:
| 'primary' | 'primary-foreground'
| 'secondary' | 'secondary-foreground'
| 'destructive' | 'destructive-foreground'
| 'muted' | 'muted-foreground'
| 'accent' | 'accent-foreground'
| 'popover' | 'popover-foreground'
| 'card' | 'card-foreground'
| 'border' | 'input' | 'ring'
| 'background' | 'foreground';
// Add custom shadow keys
shadow: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
| 'elevation-none' | 'elevation-low' | 'elevation-medium' | 'elevation-high';
// Add custom spacing keys
spacing: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '8' | '10' | '12' | '14' | '16';
}
}Now your IDE suggests all your custom keys everywhere!
License
MIT
