react-native-pixelflow
v0.6.0
Published
Professional React Native responsive design system — hybrid scaling, styled(), RTL, a11y prefs, Figma tokens, adaptive shells, and navigation bridges.
Maintainers
Readme
react-native-PixelFlow
Responsive React Native UI that stays simple.
Write normal StyleSheet numbers (or design tokens). PixelFlow scales them for phones, tablets, and foldables — without NativeWind or CSS-in-JS.
Start in 3 steps
1. Install
npm install react-native-pixelflow react-native-safe-area-context
# Expo:
npx expo install react-native-safe-area-context2. Wrap your app (order matters)
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PixelFlowProvider, configure } from 'react-native-pixelflow';
// Match your Figma frame once (optional but recommended)
configure({ figma: 'iPhone 14' });
export default function App() {
return (
<SafeAreaProvider>
<PixelFlowProvider followSystem>
{/* your screens */}
</PixelFlowProvider>
</SafeAreaProvider>
);
}3. Build a screen
Option A — easiest (tokens + components):
import { Container, Box, FText, Button, Spacer } from 'react-native-pixelflow';
export function HomeScreen() {
return (
<Container>
<Box p="md" bg="surface" radius="lg">
<FText variant="h2">Welcome</FText>
<FText color="fg.muted">Looks good on every phone size.</FText>
<Spacer size={16} />
<Button intent="primary" onPress={() => {}}>
Get started
</Button>
</Box>
</Container>
);
}Option B — paste Figma numbers into a StyleSheet:
import { usePixelSheet, FText } from 'react-native-pixelflow';
export function Card() {
const styles = usePixelSheet(() => ({
box: { padding: 16, borderRadius: 12 },
title: { fontSize: 24, marginBottom: 8 },
}));
return (
<>
<FText style={styles.title}>Title</FText>
{/* use styles.box on a View */}
</>
);
}That’s enough for most apps. Everything below is optional.
What should I use?
| I want to… | Use this |
|---|---|
| Build a screen quickly | Container, Box, FText, Button, Card, Spacer |
| Stack children with gap | Stack / HStack / VStack / AdaptiveStack |
| Form / keyboard screen | KeyboardScreen, FormField |
| Paste Figma px into styles | usePixelSheet(() => ({ … })) |
| Match my Figma frame | configure({ figma: 'iPhone 14' }) |
| Support iPad / large screens | Grid, SplitView, ContentWidth, useWindowClass, useListLayout |
| React to parent width | useContainerSize / ContainerQuery |
| Hide/show by breakpoint | Show / Hide |
| Fluid type that clamps | fluid(14, 16, 22) |
| Readable article width | useReadableWidth / maxCh |
| Tabs vs drawer nav | useAdaptiveNav |
| Fold / split-screen | useFoldState / useMultiWindow |
| Common screen patterns | useRecipes() |
| Tokenized styled components | styled(View, { p: 'md', bg: 'surface' }) |
| App shells (phone/tablet/fold) | Shell.Phone / Shell.Tablet / Shell.Tabletop |
| React Navigation colors | createNavigationTheme({ theme }) |
| Figma Variables → theme | npx pixelflow tokens ./vars.json |
| See live scale values in the app | {__DEV__ && <PixelFlowDebug />} |
| Coming from size-matters | npx pixelflow migrate ./src or scale / ms aliases |
Everyday building blocks
Box & text (design tokens)
<Box p="md" px={{ compact: 'sm', medium: 'lg' }} bg="bg.subtle" radius="lg">
<FText variant="h2" color="primary">Title</FText>
<FText color="fg.muted">Subtitle</FText>
</Box>Spacing: xs sm md lg xl xxl
Radius: none sm md lg xl full
Colors: primary, surface, text, … or semantic bg.subtle, fg.muted, border.default
You can still pass raw numbers: <Box p={16} />.
PixelSheet (auto-scale by property)
| Style key | Scales as |
|---|---|
| fontSize | font |
| padding / margin / gap | spacing |
| borderRadius | radius |
| % / flex / opacity | unchanged |
Prefer usePixelSheet inside components so styles update on rotation.
Avoid module-level StyleSheet.create({ padding: rp(16) }) — that freezes the number at import time.
Button
<Button intent="primary" size="lg" fullWidth onPress={onSave}>
Save
</Button>intent: primary | secondary | danger | ghostsize: sm | md | lg
Stack, Card, AspectBox
import {
AdaptiveStack,
AspectBox,
Card,
FText,
HStack,
Stack,
absoluteFill,
} from 'react-native-pixelflow';
<Stack gap="md">
<Card p="md" elevation="md">
<FText variant="h3">Profile</FText>
</Card>
<AdaptiveStack gap="sm" breakpoint="medium">
<AspectBox ratio="16:9">{/* Image with style={absoluteFill} */}</AspectBox>
<FText color="fg.muted">Caption</FText>
</AdaptiveStack>
<HStack gap="sm" justify="space-between">{/* … */}</HStack>
</Stack>Keyboard + form fields
import { FormField, KeyboardScreen, FText } from 'react-native-pixelflow';
import { TextInput } from 'react-native';
<KeyboardScreen>
<FormField label="Email" required helper="Work email" chrome error={errors.email}>
<TextInput value={email} onChangeText={setEmail} />
</FormField>
</KeyboardScreen>Shadows, press styles, clamp
import { clamp, createPressStyle, rf, shadow } from 'react-native-pixelflow';
const card = [styles.box, shadow('md')];
const fontSize = clamp(rf(18), 14, 22);
<Pressable
disabled={busy}
style={createPressStyle(
{
base: { padding: 12, borderRadius: 8 },
pressed: { opacity: 0.85 },
disabled: { opacity: 0.4 },
},
busy,
)}
/>Recipes, insets, lists
import {
useInsetStyle,
useListLayout,
useRecipes,
} from 'react-native-pixelflow';
const r = useRecipes();
const pad = useInsetStyle({ top: true, bottom: true, extra: 'md' });
const list = useListLayout({ minItemWidth: 160, gap: 12 });
<View style={[r.screenPadded, pad]}>
<FlatList {...list.flatListProps} data={data} renderItem={…} />
</View>Container queries, Show/Hide, fluid type
import {
ContainerQuery,
Hide,
OrientationGate,
Show,
fluid,
match,
maxCh,
rs,
useAdaptiveNav,
useImageSize,
useMultiWindow,
useReadableWidth,
} from 'react-native-pixelflow';
const columns = rs.match({ compact: 1, medium: 2, expanded: 3 });
const titleSize = fluid(18, 22, 28);
const { maxWidth } = useReadableWidth({ chars: 65 });
const img = useImageSize({ base: 120, max: 200 });
const nav = useAdaptiveNav();
const { forceCompact } = useMultiWindow();
<ContainerQuery>
{(box) => (box.meets('medium') ? <RowLayout /> : <ColumnLayout />)}
</ContainerQuery>
<Show above="medium"><TabletChrome /></Show>
<Hide below="medium"><PhoneOnlyHint /></Hide>
<OrientationGate allow="portrait">
<CheckoutForm />
</OrientationGate>
<View style={{ maxWidth: maxCh(65) }} />Wire native fold posture when you have a hinge module:
import { setFoldState } from 'react-native-pixelflow';
setFoldState({ isFolded: true, posture: 'tabletop' });styled(), shells, navigation, Figma tokens
import {
Shell,
createNavigationTheme,
styled,
useA11yPrefs,
useEstimatedItemSize,
useOverlayInsets,
usePointer,
useScaleContext,
rpWith,
} from 'react-native-pixelflow';
const Panel = styled.View({ p: 'md', bg: 'surface', radius: 'lg', ps: 'md' });
<Shell.Tablet master={<List />} detail={<Detail />} />
<Shell.Tabletop top={<Player />} bottom={<Controls />} />
// NavigationContainer theme={createNavigationTheme({ theme })}
// npx pixelflow tokens ./figma-variables.json -o src/theme/figmaTheme.ts
const { estimatedItemSize } = useEstimatedItemSize({ base: 72 });
const overlay = useOverlayInsets({ extra: 8 });
const { canHover } = usePointer();
const { a11yTheme } = useA11yPrefs();
const ctx = useScaleContext(); // pass to Reanimated: rpWith(ctx, 16)SSR / Expo Router web (avoid first-paint flash):
<PixelFlowProvider ssrWidth={390} ssrHeight={844} followSystem>
<App />
</PixelFlowProvider>Layout regression tests:
import { assertLayout, resetPixelFlow } from 'react-native-pixelflow/testing';
assertLayout({
fixture: 'phone',
expect: { widthClass: 'compact', rp16: 16 },
});Tablets & larger screens (when you need them)
Scaling alone makes iPad UI “bigger phone.” For real tablet UX, change layout:
import {
Grid,
SplitView,
ContentWidth,
useWindowClass,
} from 'react-native-pixelflow';
const { widthClass, columns, meets } = useWindowClass();
// widthClass: compact → medium → expanded → large → extraLarge
<ContentWidth>
<Grid minItemWidth={160} gap={12}>
{items.map((item) => (
<Card key={item.id} {...item} />
))}
</Grid>
</ContentWidth>
<SplitView
breakpoint="medium"
master={<List />}
detail={<Detail />}
/>Try it fast
- Expo Snack: copy
example/SnackApp.tsx— seeexample/README.md - In your app: drop in
example/ExampleScreen.tsx
Coming from react-native-size-matters?
npx pixelflow migrate ./srcOr keep familiar names:
import { scale, moderateScale, s, ms } from 'react-native-pixelflow';Advanced (optional)
Only reach for these when the basics aren’t enough.
configure({
figma: 'iPhone 14', // or { width: 390, height: 844 }
density: 'comfortable', // compact | comfortable | spacious
maxFontSizeMultiplier: 1.3,
applyAccessibilityFontScale: true,
});listFigmaPresets() shows all named frames (iPhone 14, Pixel 7, iPad Pro 11, …).
| Function | Use for |
|---|---|
| rp / rm | padding / margin |
| rf / rl / rls | font / line height / letter spacing |
| rr / ri | radius / icon |
| rw / rh | % of screen width / height |
| rsw / rsh | % of safe width / height |
| rs({ phone, tablet }) | per-device value |
| design(16) | same as rp(16) from Figma |
<Box
p={{ compact: 'sm', medium: 'md', expanded: 'lg' }}
flexDirection={['column', 'row']} // compact, medium, expanded
/>import { createVariants, useMedia, useMotion, useA11yLayout } from 'react-native-pixelflow';
const chip = createVariants({
base: { padding: 8, borderRadius: 8 },
variants: {
tone: { neutral: { opacity: 1 }, danger: { opacity: 0.9 } },
},
});
const media = useMedia();
if (media.lg || media.matches({ minWidth: 840 })) { /* wide layout */ }
const motion = useMotion(); // durations become 0 if Reduce Motion is on
const { preferStack } = useA11yLayout();import { customTheme, PixelFlowProvider } from 'react-native-pixelflow';
const brand = customTheme({
base: 'light',
colors: { primary: '#FF6B6B' },
});
<PixelFlowProvider theme={brand} density="comfortable">
<App />
</PixelFlowProvider>PixelFlow maps family names → strings (e.g. fonts.inter.bold).
It does not bundle font files.
npx expo install expo-font @expo-google-fonts/interimport { useFonts, Inter_400Regular, Inter_700Bold } from '@expo-google-fonts/inter';
const [loaded] = useFonts({ Inter_400Regular, Inter_700Bold });See src/fonts/fontRegistry.ts for the full family list.
import { mockDimensions, resetPixelFlow } from 'react-native-pixelflow/testing';
beforeEach(() => {
resetPixelFlow();
mockDimensions({ width: 390, height: 844 });
});// ESLint — warn on raw padding/fontSize outside PixelSheet
plugins: { pixelflow: require('react-native-pixelflow/eslint-plugin') },
rules: { 'pixelflow/no-raw-spacing': 'warn' }Most libs only do width / 375 scaling. PixelFlow blends width + height, damps tablets/foldables, and can change layout (grid / split) — not only enlarge padding.
Works with RN 0.70+, Hermes, Fabric, Expo. Pure JS/TS — no native modules.
Compatibility
- React Native 0.70+
- Expo (managed or bare)
- TypeScript (strict, full typings)
License
MIT — see LICENSE
