mobile-styling-unistyles
Unistyles 3.0 styling - C++ powered StyleSheet superset with zero re-renders, theming, breakpoints, variants, dynamic functions, runtime values
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-styling-unistyles/skills/mobile-styling-unistyles
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Unistyles 3.0 Patterns
Quick Guide: Unistyles 3.0 is a StyleSheet superset powered by Nitro Modules (C++/JSI). Import
StyleSheetfromreact-native-unistylesinstead ofreact-native-- same API, but with themes, breakpoints, variants, dynamic functions, and runtime values. Zero re-renders: styles update via the Shadow Tree, not React state. Configure withStyleSheet.configure()before anyStyleSheet.create(). Never spread styles ({...a, ...b}) -- use array syntax ([a, b]). Requires New Architecture (RN 0.78+).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST import StyleSheet from react-native-unistyles, NOT from react-native -- the Unistyles version is a superset that enables all features)
(You MUST call StyleSheet.configure() BEFORE any StyleSheet.create() -- configure in your entry file before importing components)
(You MUST use array syntax [styles.a, styles.b] for merging styles -- NEVER spread {...styles.a, ...styles.b} as it destroys C++ state)
(You MUST NOT use useUnistyles hook in regular components -- it forces full re-renders, defeating Unistyles' zero-render architecture)
(You MUST pass only serializable arguments to dynamic functions -- strings, numbers, booleans, arrays, objects (no functions or components))
</critical_requirements>
Auto-detection: Unistyles, react-native-unistyles, StyleSheet.configure, UnistylesRuntime, UnistylesThemes, UnistylesBreakpoints, useVariants, compoundVariants, ScopedTheme, withUnistyles, useUnistyles, miniRuntime, rt.insets, rt.screen, mq.only, Display, Hide
When to use:
- Styling React Native apps that need dynamic theming (light/dark or custom themes)
- Building responsive layouts with breakpoints and media queries across mobile and web
- Creating reusable component variants (size, color, state) without conditional logic
- Accessing runtime device values (insets, screen size, font scale) inside stylesheets
- Migrating from Unistyles 2.x to 3.0
When NOT to use:
- Apps that cannot use the New Architecture (requires RN 0.78+, Fabric)
- Expo Go apps (requires development builds with native modules)
- Minimal apps with no theme switching or responsive needs (plain StyleSheet suffices)
- Apps using a utility-class approach (consider a utility-class styling solution instead)
Key patterns covered:
- StyleSheet.configure: themes, breakpoints, settings registration
- StyleSheet.create with theme and miniRuntime (rt) access
- Variants and compound variants for reusable component styles
- Dynamic functions with serializable parameters
- Breakpoints, media queries, and Display/Hide components
- Runtime values: insets, screen dimensions, font scale, color scheme
- Scoped themes and adaptive themes
- withUnistyles for third-party component integration
- Style merging with array syntax (never spread)
Detailed Resources:
- examples/core.md - StyleSheet setup, theme access, dynamic functions, style merging
- examples/theming.md - Theme configuration, adaptive themes, scoped themes, runtime switching
- examples/responsive.md - Breakpoints, media queries, Display/Hide, runtime values
- examples/variants.md - Variants, compound variants, boolean variants, component props pattern
- reference.md - API quick reference, decision frameworks, v2-to-v3 migration
<decision_framework>
Decision Framework
Choosing the Right Styling Approach
Does the component need theme colors or runtime values?
|-- NO -> Plain StyleSheet.create (static object, no callback)
+-- YES -> StyleSheet.create((theme, rt) => ...)
|
Does it also need component-local values (props, state)?
|-- YES -> Dynamic function: style: (arg) => ({ ... })
+-- NO -> Static theme/runtime access is enough
Does the style have multiple visual variants (size, color, state)?
|-- YES -> Use variants {} inside the style
| |
| Do combinations of variants need special treatment?
| +-- YES -> Add compoundVariants []
+-- NO -> Regular style properties
Is this a third-party component that doesn't work with Unistyles styles?
|-- Try withUnistyles first (no re-renders)
+-- Only if that fails -> useUnistyles hook (causes re-renders)
Responsive: Breakpoints vs Media Queries vs Display/Hide
| Need | Solution |
|---|---|
| Simple per-breakpoint values | Breakpoint object { xs: 8, md: 16 } |
| Precise pixel ranges | mq.only.width(0, 500) |
| Show/hide entire components | <Display mq={...}> / <Hide mq={...}> |
| Orientation-specific styles | Built-in portrait / landscape breakpoints |
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Spreading styles
{...styles.a, ...styles.b}-- destroys C++ state, causes unpredictable style resolution, triggers dev warnings - Using
useUnistylesin regular components -- forces full re-renders, defeats the zero-render architecture - Calling
StyleSheet.createbeforeStyleSheet.configure-- styles won't have access to themes or breakpoints - Importing
StyleSheetfromreact-nativeinstead ofreact-native-unistyles-- styles work but lose all Unistyles features (themes, variants, breakpoints) - Passing non-serializable arguments to dynamic functions (functions, components, Promises) -- arguments are passed to C++ via
folly::dynamic, non-serializable values crash
Medium Priority Issues:
- Setting both
initialThemeandadaptiveThemes: truein configure -- they are mutually exclusive, Unistyles will throw an error - Missing Babel plugin configuration -- without it, dependency detection, ref borrowing, and scoped variants don't work
- Using
useUnistylesat the root level -- subscribes the entire app tree to every theme/runtime change - Defining breakpoints without a
0value -- at least one breakpoint must be0for CSS-like cascading to work
Gotchas & Edge Cases:
- The
bottominset is NOT dynamic for keyboard -- usert.insets.ime(input method editor) for keyboard-responsive padding - Babel plugin does NOT support moving functions outside
StyleSheet.createor reassigningtheme/rtto other variables -- the analysis is scope-bound ScopedThemedoes not work correctly aboveSuspenseboundaries -- place it inside suspended components- Metro Fast Refresh (HMR) does not propagate child changes to parent
ScopedThemecomponents -- requires manual refresh withUnistylesuniProps are lower priority than inline props -- inline props override uniProps, which override global mappings- Boolean variants use string keys
"true"and"false"-- they are distinct from adefaultvariant - All themes must share the same TypeScript type -- mismatched theme shapes cause type errors
- On web, Unistyles converts theme colors to CSS variables -- theme switching swaps a single class on
<body>, no JS recomputation UnistylesRuntimegetters are non-reactive outside StyleSheet -- useuseUnistylesorwithUnistylesfor reactive access in componentsStyleSheet.addChangeListener()(v3.1.0+) is the escape hatch for animation libraries that need runtime update notifications
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST import StyleSheet from react-native-unistyles, NOT from react-native -- the Unistyles version is a superset that enables all features)
(You MUST call StyleSheet.configure() BEFORE any StyleSheet.create() -- configure in your entry file before importing components)
(You MUST use array syntax [styles.a, styles.b] for merging styles -- NEVER spread {...styles.a, ...styles.b} as it destroys C++ state)
(You MUST NOT use useUnistyles hook in regular components -- it forces full re-renders, defeating Unistyles' zero-render architecture)
(You MUST pass only serializable arguments to dynamic functions -- strings, numbers, booleans, arrays, objects (no functions or components))
Failure to follow these rules will cause broken styles, unnecessary re-renders, and runtime crashes from the C++ core.
</critical_reminders>
Files (skills)
-
examples
-
core.md 10.3 KB
# Unistyles Core Patterns > Related: [theming.md](theming.md) for theme setup, [responsive.md](responsive.md) for breakpoints, [variants.md](variants.md) for variants --- ## Pattern 1: Static vs Themed vs Runtime StyleSheets Unistyles supports three levels of StyleSheet complexity. Choose the simplest one that meets your needs. ### Static (no callback) Identical to React Native's StyleSheet.create. No theme or runtime access. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#fff", }, separator: { height: StyleSheet.hairlineWidth, backgroundColor: "#e0e0e0", }, }); ``` ### Themed (theme callback) Access theme properties. Styles recalculate when theme changes -- no re-renders. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.background, }, title: { color: theme.colors.typography, fontWeight: "600", }, link: { color: theme.colors.link, textDecorationLine: "underline", }, })); ``` ### Themed + Runtime (theme + rt callback) Access both theme and device runtime values. Use `rt` for insets, screen dimensions, font scale, orientation, and color scheme. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create((theme, rt) => ({ safeContainer: { flex: 1, backgroundColor: theme.colors.background, paddingTop: rt.insets.top, paddingBottom: rt.insets.bottom, }, responsiveText: { color: theme.colors.typography, fontSize: rt.fontScale * 16, }, fullScreenImage: { width: rt.screen.width, height: rt.screen.height * 0.4, }, })); ``` ### miniRuntime (rt) Properties | Property | Type | Description | | ------------------------ | ----------------- | ---------------------------------------- | | `rt.insets.top` | number | Safe area top (notch/Dynamic Island) | | `rt.insets.bottom` | number | Safe area bottom (home indicator) | | `rt.insets.left` | number | Safe area left | | `rt.insets.right` | number | Safe area right | | `rt.insets.ime` | number | Keyboard height (input method editor) | | `rt.screen.width` | number | Screen width in pixels | | `rt.screen.height` | number | Screen height in pixels | | `rt.fontScale` | number | System font scale (e.g. 1.0, 1.5) | | `rt.pixelRatio` | number | Device pixel density (e.g. 2.0, 3.0) | | `rt.colorScheme` | string | `"light"`, `"dark"`, or `"unspecified"` | | `rt.isPortrait` | boolean | True when device is in portrait | | `rt.isLandscape` | boolean | True when device is in landscape | | `rt.contentSizeCategory` | string | Accessibility text size (e.g. `"Large"`) | | `rt.statusBar` | `{width, height}` | Status bar dimensions | | `rt.navigationBar` | `{width, height}` | Navigation bar dimensions (Android) | | `rt.rtl` | boolean | Right-to-left language mode | | `rt.themeName` | string? | Currently active theme name | | `rt.breakpoint` | string? | Currently active breakpoint | **Gotcha:** `rt.insets.bottom` is NOT dynamic for keyboard. Use `rt.insets.ime` for keyboard-responsive padding. --- ## Pattern 2: Dynamic Functions Dynamic functions accept component-level values as arguments. All arguments must be serializable (strings, numbers, booleans, arrays, objects). ### Basic Dynamic Function ```typescript const styles = StyleSheet.create((theme) => ({ // Dynamic function with parameters listItem: (isOdd: boolean) => ({ backgroundColor: isOdd ? theme.colors.surface : theme.colors.background, padding: 16, }), })); // Usage in JSX <View style={styles.listItem(index % 2 === 1)} /> ``` ### Multiple Parameters ```typescript const MAX_CARD_WIDTH = 400; const styles = StyleSheet.create((theme) => ({ card: (width: number, isActive: boolean) => ({ maxWidth: Math.min(width, MAX_CARD_WIDTH), backgroundColor: isActive ? theme.colors.activeCard : theme.colors.card, borderWidth: isActive ? 2 : 0, borderColor: theme.colors.accent, }), })); // Usage <View style={styles.card(containerWidth, isSelected)} /> ``` ### Mixing Static and Dynamic Styles ```typescript const styles = StyleSheet.create((theme) => ({ // Static -- no parameters, just theme access container: { flex: 1, backgroundColor: theme.colors.background, }, // Dynamic -- accepts component-level values avatar: (size: number) => ({ width: size, height: size, borderRadius: size / 2, backgroundColor: theme.colors.surface, }), })); // Static styles used normally <View style={styles.container}> {/* Dynamic styles called with arguments */} <View style={styles.avatar(48)} /> </View> ``` ### Serializable Constraint ```typescript // CORRECT -- all serializable types styles.card(42) // number styles.card("primary") // string styles.card(true) // boolean styles.card([1, 2, 3]) // array styles.card({ key: "value" }) // object // WRONG -- non-serializable types crash C++ styles.card(() => {}) // function styles.card(<Component />) // React element styles.card(new Promise(() => {})) // Promise ``` --- ## Pattern 3: Style Merging ### Correct: Array Syntax ```typescript // Merge multiple styles -- order matters (last wins) <View style={[styles.container, styles.overlay]} /> // Conditional styles <View style={[styles.card, isFocused && styles.cardFocused]} /> // Dynamic + static merge <View style={[styles.card(isOdd), styles.shadow]} /> // Multiple conditions <View style={[ styles.button, isDisabled && styles.disabled, isPressed && styles.pressed, ]} /> ``` ### Wrong: Spreading ```typescript // NEVER do this -- destroys C++ state <View style={{ ...styles.container, ...styles.overlay }} /> // NEVER do this either -- inline overrides also break state <View style={{ ...styles.container, backgroundColor: "red" }} /> ``` **What happens with spreading:** Unistyles detects the lost C++ state in dev mode, restores it, but merges in unpredictable order. Production builds may silently produce wrong styles. --- ## Pattern 4: withUnistyles for Third-Party Components Use `withUnistyles` only for third-party components that cannot receive Unistyles styles through normal `style` props. Regular RN components (`View`, `Text`, `Pressable`) work without it. ### Auto-Mapping (Components with style Prop) ```typescript import { withUnistyles } from "react-native-unistyles"; import { BlurView } from "expo-blur"; const UniBlurView = withUnistyles(BlurView); // style prop is automatically mapped <UniBlurView style={styles.blur} intensity={50} /> ``` ### Static Prop Mappings ```typescript import { withUnistyles } from "react-native-unistyles"; import { Switch } from "react-native"; // Map theme values to component props const UniSwitch = withUnistyles(Switch, (theme) => ({ trackColor: { false: theme.colors.dimmed, true: theme.colors.tint, }, thumbColor: theme.colors.background, })); <UniSwitch value={isEnabled} onValueChange={setIsEnabled} /> ``` ### Dynamic Props (uniProps) ```typescript // Use uniProps when mappings depend on component state <UniSwitch value={isEnabled} onValueChange={setIsEnabled} uniProps={(theme, rt) => ({ trackColor: { false: theme.colors.dimmed, true: isEnabled ? theme.colors.primary : theme.colors.secondary, }, })} /> ``` ### Props Priority (Lowest to Highest) 1. Global mappings (second arg of `withUnistyles`) 2. `uniProps` function 3. Inline props on the component Inline props always win over `uniProps`, which win over global mappings. ### When to Use What | Component Type | Solution | | ------------------------------------------- | ------------------------------------------------- | | React Native built-ins (`View`, `Text`) | Normal `style` prop -- no wrapper needed | | Third-party with `style` prop | `withUnistyles(Component)` -- auto-maps style | | Third-party with custom props (color, tint) | `withUnistyles(Component, mappings)` + `uniProps` | | Last resort / migration from v2 | `useUnistyles()` hook (causes re-renders) | --- ## Pattern 5: UnistylesRuntime (Imperative Access) `UnistylesRuntime` provides read/write access to Unistyles state from anywhere -- including outside React components. ### Reading Values ```typescript import { UnistylesRuntime } from "react-native-unistyles"; // Current state const currentTheme = UnistylesRuntime.themeName; // "light" | "dark" | ... const currentBreakpoint = UnistylesRuntime.breakpoint; // "xs" | "sm" | ... const { width, height } = UnistylesRuntime.screen; const isPortrait = UnistylesRuntime.isPortrait; const colorScheme = UnistylesRuntime.colorScheme; // "light" | "dark" | "unspecified" const insets = UnistylesRuntime.insets; // { top, bottom, left, right, ime } ``` ### Mutating State ```typescript // Switch theme UnistylesRuntime.setTheme("dark"); // Toggle adaptive themes UnistylesRuntime.setAdaptiveThemes(true); // Update theme at runtime (e.g. user picks accent color) UnistylesRuntime.updateTheme("light", (currentTheme) => ({ ...currentTheme, colors: { ...currentTheme.colors, primary: userSelectedColor, }, })); // System bars UnistylesRuntime.statusBar.setHidden(true); UnistylesRuntime.navigationBar.setHidden(true); UnistylesRuntime.setImmersiveMode(true); // hides both // Root view background UnistylesRuntime.setRootViewBackgroundColor("#000"); ``` **Important:** `UnistylesRuntime` getters are non-reactive outside StyleSheet callbacks. Reading `UnistylesRuntime.themeName` in a component does not subscribe to changes. Use `useUnistyles()` or `withUnistyles` when you need reactive access in JSX. -
responsive.md 6.2 KB
# Unistyles Responsive Patterns > Related: [core.md](core.md) for StyleSheet basics, [theming.md](theming.md) for theme setup --- ## Pattern 1: Breakpoint Object Syntax Convert any style property to a breakpoint object. Values cascade upward -- `xs` applies until a larger breakpoint overrides it. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create((theme) => ({ container: { padding: { xs: theme.spacing.sm, // 0px and up md: theme.spacing.md, // 768px and up xl: theme.spacing.xl, // 1200px and up }, flexDirection: { xs: "column" as const, md: "row" as const, }, }, sidebar: { width: { xs: "100%" as const, md: 280, lg: 320, }, }, })); ``` **Why good:** values cascade like CSS media queries -- `xs` stays active until `md` overrides it. No need to repeat values at every breakpoint. ### Breakpoints with Nested Objects Breakpoint objects work with complex style properties like `transform` and `shadowOffset`: ```typescript const styles = StyleSheet.create(() => ({ card: { transform: { xs: [{ scale: 0.9 }], md: [{ scale: 1.0 }], }, shadowOffset: { xs: { width: 0, height: 1 }, md: { width: 0, height: 4 }, }, }, })); ``` --- ## Pattern 2: Media Queries (mq) For precise pixel ranges that don't align with named breakpoints, use the `mq` utility. Media queries always have higher priority than breakpoints. ```typescript import { StyleSheet, mq } from "react-native-unistyles"; const styles = StyleSheet.create((theme) => ({ sidebar: { display: { // Hidden on small screens, visible on 768px+ [mq.only.width(0, 768)]: "none" as const, [mq.only.width(768)]: "flex" as const, }, }, grid: { flexDirection: { // Column layout below 500px width, row above [mq.only.width(0, 500)]: "column" as const, [mq.only.width(500)]: "row" as const, }, }, })); ``` ### mq Syntax Reference ```typescript // Width-only ranges mq.only.width(0, 500); // 0px to 499px mq.only.width(500); // 500px and up mq.only.width(null, 800); // 0px to 799px mq.only.width("sm", "md"); // sm breakpoint to md breakpoint // Height-only ranges mq.only.height(300, 600); // 300px to 599px height mq.only.height(600); // 600px height and up // Combined width AND height mq.width(240, 380).and.height(300); // width 240-379 AND height 300+ mq.height(500).and.width("sm"); // height 500+ AND width from sm breakpoint ``` **Gotcha:** Invalid ranges like `mq.only.width(500, 200)` or `mq.only.width("xl", "sm")` are silently ignored. --- ## Pattern 3: Display and Hide Components Conditionally render entire components based on breakpoints or media queries. These are simple if/else wrappers -- no extra view layers added. ```typescript import { Display, Hide, mq } from "react-native-unistyles"; function ResponsiveLayout() { return ( <View style={styles.container}> {/* Show sidebar only on md and up */} <Display mq={mq.only.width("md")}> <Sidebar /> </Display> {/* Hide desktop header on small screens */} <Hide mq={mq.only.width(0, 768)}> <DesktopHeader /> </Hide> {/* Show mobile nav only on small screens */} <Display mq={mq.only.width(0, 768)}> <MobileNav /> </Display> <MainContent /> </View> ); } ``` **Why good:** no conditional logic in component code, no wasted renders -- Display/Hide evaluate at the C++ level --- ## Pattern 4: Built-in Orientation Breakpoints Even without custom breakpoints, Unistyles provides `portrait` and `landscape` breakpoints that resolve to the device's width in each orientation. ```typescript const styles = StyleSheet.create((theme) => ({ header: { height: { portrait: 120, landscape: 64, }, flexDirection: { portrait: "column" as const, landscape: "row" as const, }, }, })); ``` --- ## Pattern 5: Runtime Values for Responsive Layouts Use the miniRuntime (`rt`) for device-specific values that update automatically. ### Safe Area Insets ```typescript const styles = StyleSheet.create((theme, rt) => ({ screen: { flex: 1, paddingTop: rt.insets.top, paddingBottom: rt.insets.bottom, paddingLeft: rt.insets.left, paddingRight: rt.insets.right, }, })); ``` ### Keyboard-Aware Padding ```typescript const styles = StyleSheet.create((theme, rt) => ({ input: { // rt.insets.ime = keyboard height (input method editor) // rt.insets.bottom = static safe area bottom paddingBottom: rt.insets.ime > 0 ? rt.insets.ime : rt.insets.bottom, }, })); ``` **Gotcha:** `rt.insets.bottom` is the static safe area inset (home indicator). It does NOT change when the keyboard appears. Use `rt.insets.ime` for keyboard-responsive padding. ### Font Scale Aware Styles ```typescript const BASE_FONT_SIZE = 16; const MAX_FONT_SIZE = 24; const styles = StyleSheet.create((_theme, rt) => ({ body: { fontSize: Math.min(rt.fontScale * BASE_FONT_SIZE, MAX_FONT_SIZE), }, })); ``` ### Screen Dimension Dependent Styles ```typescript const IMAGE_ASPECT_RATIO = 0.5625; // 16:9 inverted const COLUMN_COUNT = 2; const COLUMN_GAP = 16; const styles = StyleSheet.create((_theme, rt) => ({ heroImage: { width: rt.screen.width, height: rt.screen.width * IMAGE_ASPECT_RATIO, }, gridItem: { width: (rt.screen.width - COLUMN_GAP * (COLUMN_COUNT + 1)) / COLUMN_COUNT, }, })); ``` --- ## Pattern 6: Mixing Breakpoints and Media Queries Breakpoints and mq can coexist on the same style. Media queries always take priority when both match. ```typescript const styles = StyleSheet.create((theme) => ({ container: { padding: { xs: 8, md: 16, // mq overrides breakpoint when its range matches [mq.only.width(0, 320)]: 4, }, }, })); ``` ### Native vs Web Breakpoint Behavior - **Native (iOS/Android):** Breakpoints are calculated based on screen pixels (default) or points (`nativeBreakpointsMode: "points"`) - **Web:** Breakpoints automatically generate CSS `@media` queries -- no JavaScript recalculation on resize ```typescript // To use screen points instead of pixels on native: StyleSheet.configure({ breakpoints, settings: { nativeBreakpointsMode: "points", }, }); ``` -
theming.md 7.1 KB
# Unistyles Theming Patterns > Related: [core.md](core.md) for StyleSheet basics, [responsive.md](responsive.md) for breakpoints --- ## Pattern 1: Theme Definition and Type Safety Themes are plain JavaScript objects. All themes must share the same TypeScript type. ### Define Themes ```typescript // unistyles.ts const lightTheme = { colors: { background: "#FCFAF8", foreground: "#EDEAE6", typography: "#1B140C", dimmed: "#ECE8E4", tint: "#9A734C", primary: "#2563EB", secondary: "#64748B", surface: "#FFFFFF", error: "#DC2626", link: "#1E3799", accents: { warning: "#F6E58D", success: "#BADC58", danger: "#FF7979", }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, // Functions in themes are fine gap: (multiplier: number) => multiplier * 8, } as const; const darkTheme = { colors: { background: "#221A11", foreground: "#332618", typography: "#FFFFFF", dimmed: "#A8A198", tint: "#C9AD92", primary: "#3B82F6", secondary: "#94A3B8", surface: "#2D2D2D", error: "#EF4444", link: "#0C2461", accents: { warning: "#F9CA24", success: "#6AB04C", danger: "#EB4D4B", }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, gap: (multiplier: number) => multiplier * 8, } as const; ``` ### TypeScript Declarations ```typescript // Must be in the same file or imported before StyleSheet.configure type AppThemes = { light: typeof lightTheme; dark: typeof darkTheme; }; declare module "react-native-unistyles" { export interface UnistylesThemes extends AppThemes {} } ``` **Why this matters:** Without the module declaration, `theme` in `StyleSheet.create((theme) => ...)` is typed as `unknown`. With it, you get full autocompletion for `theme.colors.primary`, `theme.spacing.md`, etc. --- ## Pattern 2: StyleSheet.configure Call `StyleSheet.configure` once, in your entry file, before any component imports. ### Basic Setup ```typescript // unistyles.ts -- import this FIRST in your entry file import { StyleSheet } from "react-native-unistyles"; StyleSheet.configure({ themes: { light: lightTheme, dark: darkTheme, }, breakpoints: { xs: 0, // Required: at least one must be 0 sm: 576, md: 768, lg: 992, xl: 1200, }, settings: { initialTheme: "light", }, }); ``` ### Entry File Import Order ```typescript // index.ts (entry point) import "./unistyles"; // MUST be first -- before any component imports import "expo-router/entry"; ``` ### Settings Options | Setting | Type | Description | | ----------------------- | ------------------------ | -------------------------------------------------------------- | | `initialTheme` | string or `() => string` | Sets the first theme. Function must be synchronous. | | `adaptiveThemes` | boolean | Auto-switch theme based on device color scheme | | `CSSVars` | boolean | Enable CSS variables on web (default: true) | | `nativeBreakpointsMode` | `"pixels"` or `"points"` | How breakpoints are calculated on native (default: `"pixels"`) | **Mutually exclusive:** `initialTheme` and `adaptiveThemes` cannot both be set -- Unistyles throws an error. ### Initial Theme from Storage ```typescript StyleSheet.configure({ themes: { light: lightTheme, dark: darkTheme }, settings: { // Synchronous function -- read from MMKV or similar sync storage initialTheme: () => { const stored = storage.getString("theme"); return stored === "dark" ? "dark" : "light"; }, }, }); ``` --- ## Pattern 3: Adaptive Themes Adaptive themes automatically follow the device's color scheme (light/dark mode). Theme names must match: `"light"` and `"dark"`. ```typescript StyleSheet.configure({ themes: { light: lightTheme, dark: darkTheme, }, settings: { adaptiveThemes: true, }, }); ``` ### Runtime Control ```typescript import { UnistylesRuntime } from "react-native-unistyles"; // Check if adaptive themes are active const isAdaptive = UnistylesRuntime.hasAdaptiveThemes; // Disable adaptive themes (user chose manual theme) UnistylesRuntime.setAdaptiveThemes(false); // Then set a specific theme UnistylesRuntime.setTheme("dark"); // Re-enable adaptive themes UnistylesRuntime.setAdaptiveThemes(true); // Read device color scheme const scheme = UnistylesRuntime.colorScheme; // "light" | "dark" | "unspecified" ``` --- ## Pattern 4: Theme Switching at Runtime ```typescript import { UnistylesRuntime } from "react-native-unistyles"; // Switch to a named theme UnistylesRuntime.setTheme("dark"); // Read current theme name const current = UnistylesRuntime.themeName; // "dark" // Get the full theme object const theme = UnistylesRuntime.getTheme("light"); ``` **Important:** `setTheme` is incompatible with `adaptiveThemes: true`. Disable adaptive themes first if you want manual control. --- ## Pattern 5: Runtime Theme Updates Modify theme properties without switching themes. Useful for user-customizable accent colors. ```typescript import { UnistylesRuntime } from "react-native-unistyles"; // Update specific properties in current theme UnistylesRuntime.updateTheme("light", (currentTheme) => ({ ...currentTheme, colors: { ...currentTheme.colors, primary: userSelectedColor, tint: userSelectedAccent, }, })); ``` All components using the updated theme properties recalculate their styles automatically. No re-renders. --- ## Pattern 6: Scoped Themes Force a specific theme on a subtree, regardless of the global theme. Useful for camera screens (always dark), modals, or preview components. ```typescript import { ScopedTheme } from "react-native-unistyles"; // Force dark theme on camera screen function CameraScreen() { return ( <ScopedTheme name="dark"> <View style={styles.container}> <Text style={styles.label}>Camera Preview</Text> </View> </ScopedTheme> ); } // Invert the adaptive theme (light when global is dark, vice versa) <ScopedTheme invertedAdaptive> <View style={styles.invertedSection} /> </ScopedTheme> // Reset to parent theme inside a scoped section <ScopedTheme name="dark"> <View> <Text>Dark themed</Text> <ScopedTheme reset> <Text>Back to global theme</Text> </ScopedTheme> </View> </ScopedTheme> ``` ### ScopedTheme Props | Prop | Type | Description | | ------------------ | ------- | ------------------------------------------ | | `name` | string | Force a specific theme by name | | `invertedAdaptive` | boolean | Use the opposite of current adaptive theme | | `reset` | boolean | Reset to parent/global theme | ### Gotchas - Place `ScopedTheme` **inside** suspended components, not above `Suspense` boundaries - Metro HMR does not propagate child changes to parent `ScopedTheme` -- requires manual refresh - `ScopedTheme` does not use React Context (by design, for performance) -
variants.md 7.1 KB
# Unistyles Variants Patterns > Related: [core.md](core.md) for StyleSheet basics, [theming.md](theming.md) for themes --- ## Pattern 1: Basic Variants Define named groups of style options inside `variants`. Select the active combination with `styles.useVariants()`. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create((theme) => ({ badge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, variants: { status: { success: { backgroundColor: theme.colors.accents.success }, warning: { backgroundColor: theme.colors.accents.warning }, error: { backgroundColor: theme.colors.accents.danger }, }, size: { sm: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8 }, md: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12 }, lg: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16 }, }, }, }, })); // In component styles.useVariants({ status: "success", size: "md" }); <View style={styles.badge}> <Text>Active</Text> </View> ``` **Why good:** eliminates conditional style objects, TypeScript infers valid status/size combinations, all variant logic lives in the stylesheet --- ## Pattern 2: Boolean Variants Use `"true"` and `"false"` as string keys for toggle-style variants. These are distinct from a `default` variant. ```typescript const styles = StyleSheet.create((theme) => ({ input: { borderWidth: 1, borderColor: theme.colors.dimmed, padding: 12, borderRadius: 8, variants: { isDisabled: { true: { opacity: 0.5, backgroundColor: theme.colors.dimmed, }, false: { opacity: 1, backgroundColor: theme.colors.surface, }, }, hasError: { true: { borderColor: theme.colors.error, borderWidth: 2, }, // "false" variant is optional -- base styles apply when not "true" }, }, }, })); // In component styles.useVariants({ isDisabled: false, hasError: hasValidationError, }); ``` **Gotcha:** Boolean variant keys are strings `"true"` and `"false"`, but you pass actual booleans to `useVariants`. You don't need to define both `"true"` and `"false"` -- omitting one means the base styles apply for that value. --- ## Pattern 3: Default Variants Define a `default` key that applies when no variant is selected (undefined) or when `useVariants({})` is called. ```typescript const styles = StyleSheet.create((theme) => ({ button: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, variants: { color: { default: { backgroundColor: theme.colors.surface }, primary: { backgroundColor: theme.colors.primary }, danger: { backgroundColor: theme.colors.error }, }, }, }, })); // All of these use the "default" variant: styles.useVariants({}); styles.useVariants(undefined); styles.useVariants({ color: undefined }); ``` --- ## Pattern 4: Compound Variants Apply additional styles when multiple variant conditions are met simultaneously. Compound variant styles override regular variant styles. ```typescript const styles = StyleSheet.create((theme) => ({ text: { fontSize: 14, variants: { weight: { normal: { fontWeight: "400" }, bold: { fontWeight: "700" }, }, color: { default: { color: theme.colors.typography }, primary: { color: theme.colors.primary }, link: { color: theme.colors.link }, }, }, compoundVariants: [ // When bold AND link, add underline { weight: "bold", color: "link", styles: { textDecorationLine: "underline", }, }, // When bold AND primary, increase size { weight: "bold", color: "primary", styles: { fontSize: 16, }, }, ], }, })); ``` **Precedence order (lowest to highest):** 1. Base styles (outside `variants`) 2. Regular variant styles 3. Compound variant styles (always win over regular variants) --- ## Pattern 5: Component Props Pattern Wire variant selections to component props using the `UnistylesVariants` utility type. This provides full type safety from StyleSheet to JSX. ```typescript import { StyleSheet } from "react-native-unistyles"; import type { UnistylesVariants } from "react-native-unistyles"; import { View, Text, Pressable } from "react-native"; const styles = StyleSheet.create((theme) => ({ button: { borderRadius: 8, alignItems: "center" as const, justifyContent: "center" as const, variants: { variant: { filled: { backgroundColor: theme.colors.primary }, outlined: { backgroundColor: "transparent", borderWidth: 1, borderColor: theme.colors.primary, }, ghost: { backgroundColor: "transparent" }, }, size: { sm: { paddingHorizontal: 12, paddingVertical: 6 }, md: { paddingHorizontal: 16, paddingVertical: 10 }, lg: { paddingHorizontal: 24, paddingVertical: 14 }, }, }, }, })); // Derive props type from the stylesheet type ButtonVariants = UnistylesVariants<typeof styles>; interface ButtonProps extends ButtonVariants { title: string; onPress: () => void; } export function Button({ title, onPress, variant = "filled", size = "md" }: ButtonProps) { // useVariants binds the component to the selected variants styles.useVariants({ variant, size }); return ( <Pressable style={styles.button} onPress={onPress}> <Text>{title}</Text> </Pressable> ); } // Usage -- TypeScript enforces valid variant combinations <Button title="Save" onPress={handleSave} variant="filled" size="lg" /> <Button title="Cancel" onPress={handleCancel} variant="ghost" size="sm" /> ``` **Why good:** `UnistylesVariants` derives the exact variant options from the stylesheet -- adding a new variant in StyleSheet automatically adds it to the component's type --- ## Pattern 6: Multi-Style Variants When multiple style keys in the same StyleSheet need the same variant groups, define identical variant keys in each. This is common for components with separate container and text styles. ```typescript const styles = StyleSheet.create((theme) => ({ container: { padding: 16, borderRadius: 8, variants: { intent: { info: { backgroundColor: theme.colors.accents.success }, warning: { backgroundColor: theme.colors.accents.warning }, error: { backgroundColor: theme.colors.accents.danger }, }, }, }, label: { fontWeight: "600", variants: { intent: { info: { color: "#1a472a" }, warning: { color: "#7c4a03" }, error: { color: "#7f1d1d" }, }, }, }, })); // Single useVariants call controls all style keys styles.useVariants({ intent: "warning" }); <View style={styles.container}> <Text style={styles.label}>Warning message</Text> </View> ``` **Important:** All variant options must appear in each style key that uses that variant group. If `container` has `info/warning/error`, then `label` must also define all three. Missing options cause TypeScript errors.
-
-
reference.md 9.5 KB
# Unistyles Quick Reference ## API Cheat Sheet ### Imports ```typescript import { StyleSheet, // StyleSheet.create, StyleSheet.configure UnistylesRuntime, // Imperative read/write access ScopedTheme, // Force theme on subtree Display, // Show children at breakpoint/mq Hide, // Hide children at breakpoint/mq mq, // Media query utility withUnistyles, // HOC for third-party components useUnistyles, // Hook (avoid -- causes re-renders) } from "react-native-unistyles"; import type { UnistylesVariants, // Derive variant props from stylesheet UnistylesThemes, // Module augmentation target UnistylesBreakpoints, // Module augmentation target } from "react-native-unistyles"; ``` ### StyleSheet Methods | Method | Purpose | | --------------------------------------------------------- | ---------------------------------------------------- | | `StyleSheet.create(styles)` | Static styles (no theme) | | `StyleSheet.create((theme) => styles)` | Theme-dependent styles | | `StyleSheet.create((theme, rt) => styles)` | Theme + runtime-dependent styles | | `StyleSheet.configure({ themes, breakpoints, settings })` | One-time setup | | `StyleSheet.hairlineWidth` | Thinnest drawable line | | `StyleSheet.absoluteFillObject` | `{ position: 'absolute', left/top/right/bottom: 0 }` | | `StyleSheet.compose(a, b)` | Compose two styles | | `StyleSheet.flatten(styles)` | Flatten nested styles | | `StyleSheet.addChangeListener(cb)` | Subscribe to dependency changes (v3.1.0+) | ### UnistylesRuntime Properties | Property | Type | Reactive? | | ---------------------------- | -------------------------------------- | --------- | | `themeName` | `string?` | No\* | | `breakpoint` | `string?` | No\* | | `screen` | `{ width, height }` | No\* | | `isPortrait` / `isLandscape` | boolean | No\* | | `colorScheme` | `"light"` / `"dark"` / `"unspecified"` | No\* | | `insets` | `{ top, bottom, left, right, ime }` | No\* | | `statusBar` | `{ width, height }` | No\* | | `navigationBar` | `{ width, height }` | No\* | | `pixelRatio` | number | No\* | | `fontScale` | number | No\* | | `rtl` | boolean | No\* | | `hasAdaptiveThemes` | boolean | No\* | | `contentSizeCategory` | string | No\* | | `breakpoints` | object | No\* | | `getTheme(name?)` | Theme object | No\* | \*Non-reactive outside StyleSheet callbacks. Use `useUnistyles()` or `withUnistyles` for reactive access in components. ### UnistylesRuntime Methods | Method | Purpose | | ----------------------------------- | ----------------------- | | `setTheme(name)` | Switch active theme | | `setAdaptiveThemes(bool)` | Toggle adaptive theming | | `updateTheme(name, updater)` | Modify theme at runtime | | `statusBar.setHidden(bool)` | Show/hide status bar | | `navigationBar.setHidden(bool)` | Show/hide nav bar | | `setImmersiveMode(bool)` | Hide both bars | | `setRootViewBackgroundColor(color)` | Set root background | --- ## Decision Framework ### When to Use Which API ``` Do you need theme colors in styles? |-- NO -> StyleSheet.create({ ... }) -- static, no callback +-- YES -> StyleSheet.create((theme) => ...) | Do you also need device values (insets, screen, fontScale)? +-- YES -> StyleSheet.create((theme, rt) => ...) Do styles depend on component props/state? |-- YES -> Dynamic function: style: (arg) => ({ ... }) +-- NO -> Static or theme-only style is enough Need different visual modes (size, color, state)? |-- YES -> Use variants: { ... } inside the style +-- NO -> Regular style properties Need to conditionally show/hide entire components by screen size? |-- YES -> <Display mq={...}> / <Hide mq={...}> +-- NO -> Use breakpoint objects on style properties ``` --- ## v2 to v3 Migration Checklist ### Configuration | v2 | v3 | | ------------------------------------ | -------------------------------------------- | | `UnistylesRegistry.addConfig()` | `StyleSheet.configure()` | | `UnistylesRegistry.addThemes()` | `StyleSheet.configure({ themes: ... })` | | `UnistylesRegistry.addBreakpoints()` | `StyleSheet.configure({ breakpoints: ... })` | | `useInitialTheme(...)` | `settings: { initialTheme: "..." }` | | `UnistylesProvider` | Remove -- not needed in v3 | ### StyleSheet | v2 | v3 | | ----------------------------------------- | -------------------------------------- | | `createStyleSheet(...)` | `StyleSheet.create(...)` | | `useStyles(stylesheet)` | Use styles directly -- no hook needed | | `useStyles(stylesheet, { variant: "x" })` | `styles.useVariants({ variant: "x" })` | | `UnistylesRuntime.hairlineWidth` | `StyleSheet.hairlineWidth` | ### Hooks and Components | v2 | v3 | | ------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `useStyles()` for theme access | `StyleSheet.create((theme) => ...)` in styles, or `withUnistyles` / `useUnistyles` in components | | Breakpoint conditionals in JS | `<Display mq={...}>` / `<Hide mq={...}>` | | `UnistylesRuntime.statusBar.setColor()` | Removed (Android 15 deprecation) | | `UnistylesRuntime.navigationBar.setColor()` | Removed (Android 15 deprecation) | | `addPlugin()` / `removePlugin()` | Removed (plugins eliminated) | ### Style Syntax | v2 | v3 | | ---------------------------------- | ------------------------------------------- | | `{ ...styles.a, ...styles.b }` | `[styles.a, styles.b]` (array syntax) | | Color methods with alpha parameter | Color methods accept single color parameter | ### Removed Settings - `plugins` -- eliminated entirely - `experimentalCSSMediaQueries` -- now always enabled - `windowResizeDebounceTimeMs` -- no debouncing - `disableAnimatedInsets` -- insets no longer re-render ### Insets | v2 | v3 | | -------------------------------- | ------------------------------------------------------------------- | | `insets.bottom` (keyboard-aware) | `insets.ime` for keyboard, `insets.bottom` is static safe area only | --- ## Requirements | Requirement | Minimum | | --------------- | -------------------------------------------- | | React Native | 0.78.0+ | | Architecture | New Architecture (Fabric) required | | Expo SDK | 53+ (if using Expo) | | Xcode | 16+ recommended | | Peer dependency | `react-native-nitro-modules` (fixed version) | ### Babel Plugin ```javascript // babel.config.js plugins: [["react-native-unistyles/plugin", { root: "src" }]]; ``` | Option | Purpose | | -------------------- | ------------------------------------------------------ | | `root` (required) | Folder containing components to process | | `autoProcessImports` | Process files with specific imports (monorepo support) | | `autoRemapImports` | Remap uncommon imports to Unistyles components | | `autoProcessPaths` | Extend processing to node_modules packages | | `debug` | Log detected dependencies to console | ### TypeScript Module Augmentation ```typescript type AppThemes = { light: typeof lightTheme; dark: typeof darkTheme }; type AppBreakpoints = typeof breakpoints; declare module "react-native-unistyles" { export interface UnistylesThemes extends AppThemes {} export interface UnistylesBreakpoints extends AppBreakpoints {} } ``` -
SKILL.md 16.1 KB
--- name: mobile-styling-unistyles description: Unistyles 3.0 styling - C++ powered StyleSheet superset with zero re-renders, theming, breakpoints, variants, dynamic functions, runtime values --- # Unistyles 3.0 Patterns > **Quick Guide:** Unistyles 3.0 is a StyleSheet superset powered by Nitro Modules (C++/JSI). Import `StyleSheet` from `react-native-unistyles` instead of `react-native` -- same API, but with themes, breakpoints, variants, dynamic functions, and runtime values. Zero re-renders: styles update via the Shadow Tree, not React state. Configure with `StyleSheet.configure()` before any `StyleSheet.create()`. Never spread styles (`{...a, ...b}`) -- use array syntax (`[a, b]`). Requires New Architecture (RN 0.78+). --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST import `StyleSheet` from `react-native-unistyles`, NOT from `react-native` -- the Unistyles version is a superset that enables all features)** **(You MUST call `StyleSheet.configure()` BEFORE any `StyleSheet.create()` -- configure in your entry file before importing components)** **(You MUST use array syntax `[styles.a, styles.b]` for merging styles -- NEVER spread `{...styles.a, ...styles.b}` as it destroys C++ state)** **(You MUST NOT use `useUnistyles` hook in regular components -- it forces full re-renders, defeating Unistyles' zero-render architecture)** **(You MUST pass only serializable arguments to dynamic functions -- strings, numbers, booleans, arrays, objects (no functions or components))** </critical_requirements> --- **Auto-detection:** Unistyles, react-native-unistyles, StyleSheet.configure, UnistylesRuntime, UnistylesThemes, UnistylesBreakpoints, useVariants, compoundVariants, ScopedTheme, withUnistyles, useUnistyles, miniRuntime, rt.insets, rt.screen, mq.only, Display, Hide **When to use:** - Styling React Native apps that need dynamic theming (light/dark or custom themes) - Building responsive layouts with breakpoints and media queries across mobile and web - Creating reusable component variants (size, color, state) without conditional logic - Accessing runtime device values (insets, screen size, font scale) inside stylesheets - Migrating from Unistyles 2.x to 3.0 **When NOT to use:** - Apps that cannot use the New Architecture (requires RN 0.78+, Fabric) - Expo Go apps (requires development builds with native modules) - Minimal apps with no theme switching or responsive needs (plain StyleSheet suffices) - Apps using a utility-class approach (consider a utility-class styling solution instead) **Key patterns covered:** - StyleSheet.configure: themes, breakpoints, settings registration - StyleSheet.create with theme and miniRuntime (rt) access - Variants and compound variants for reusable component styles - Dynamic functions with serializable parameters - Breakpoints, media queries, and Display/Hide components - Runtime values: insets, screen dimensions, font scale, color scheme - Scoped themes and adaptive themes - withUnistyles for third-party component integration - Style merging with array syntax (never spread) **Detailed Resources:** - [examples/core.md](examples/core.md) - StyleSheet setup, theme access, dynamic functions, style merging - [examples/theming.md](examples/theming.md) - Theme configuration, adaptive themes, scoped themes, runtime switching - [examples/responsive.md](examples/responsive.md) - Breakpoints, media queries, Display/Hide, runtime values - [examples/variants.md](examples/variants.md) - Variants, compound variants, boolean variants, component props pattern - [reference.md](reference.md) - API quick reference, decision frameworks, v2-to-v3 migration --- <philosophy> ## Philosophy Unistyles 3.0 is a **StyleSheet superset** -- if you know React Native's `StyleSheet.create`, you know 80% of Unistyles. The remaining 20% is what makes it powerful: themes, responsive breakpoints, variants, and runtime values, all managed in C++ via JSI with **zero React re-renders**. **How it works:** 1. **Babel plugin** analyzes StyleSheets at build time, detecting dependencies (theme, runtime, breakpoints) 2. **C++ core** reconstructs StyleSheets natively and tracks which styles depend on what 3. **Shadow Tree updates** bypass React entirely -- when a theme changes or the device rotates, only the affected ShadowNodes update their styles directly **Core principles:** 1. **Zero re-renders** -- No hooks, no context, no state updates for style changes. The C++ core updates the Shadow Tree directly. 2. **StyleSheet superset** -- Same API as React Native's StyleSheet. Replace the import, keep your code. 3. **Type-safe themes** -- TypeScript declarations ensure full autocompletion for theme properties. 4. **Selective recalculation** -- Only styles that depend on a changed value (theme, breakpoint, inset) are recalculated. 5. **Cross-platform** -- Same styles work on iOS, Android, and web (with automatic CSS class generation for web). **When Unistyles adds value over plain StyleSheet:** - Multiple themes (dark/light/custom) with instant switching - Responsive layouts that adapt to screen size, orientation, or device type - Component variants (primary/secondary, small/large) without conditional style logic - Runtime-dependent styles (safe area insets, font scale, keyboard height) </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: StyleSheet.create with Theme and Runtime Replace `react-native` import with `react-native-unistyles`. The create function accepts a callback with `theme` and `rt` (miniRuntime) for dynamic styles. Static styles (no theme/runtime) work identically to plain StyleSheet. ```typescript import { StyleSheet } from "react-native-unistyles"; const styles = StyleSheet.create((theme, rt) => ({ container: { flex: 1, backgroundColor: theme.colors.background, paddingTop: rt.insets.top, }, text: { color: theme.colors.typography, fontSize: rt.fontScale * 16, }, // Static styles work exactly like plain StyleSheet separator: { height: StyleSheet.hairlineWidth, backgroundColor: "#ccc", }, })); ``` **Why good:** theme and rt are injected by the C++ core -- no hooks needed, no re-renders when theme or device values change See [examples/core.md](examples/core.md) for static vs themed vs runtime StyleSheets and the full miniRuntime property list. --- ### Pattern 2: Variants and Compound Variants Define style variations inside `variants` -- then select them with `styles.useVariants()`. Compound variants apply styles when multiple variant conditions are met simultaneously. ```typescript const styles = StyleSheet.create((theme) => ({ button: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, variants: { color: { primary: { backgroundColor: theme.colors.primary }, secondary: { backgroundColor: theme.colors.secondary }, }, size: { sm: { paddingHorizontal: 8, paddingVertical: 4 }, lg: { paddingHorizontal: 24, paddingVertical: 12 }, }, }, compoundVariants: [ { color: "primary", size: "lg", styles: { borderWidth: 2, borderColor: theme.colors.accent }, }, ], }, })); // In component -- call useVariants to select active variants styles.useVariants({ color: "primary", size: "lg" }); ``` **Why good:** eliminates conditional style objects, compound variants reduce complex if/else chains, TypeScript infers valid variant combinations See [examples/variants.md](examples/variants.md) for boolean variants, default variants, the component props pattern with `UnistylesVariants`, and multi-style variants. --- ### Pattern 3: Dynamic Functions When styles depend on component-level values (not just theme/runtime), use dynamic functions. Arguments must be serializable (strings, numbers, booleans, arrays, objects). ```typescript const styles = StyleSheet.create((theme) => ({ card: (isHighlighted: boolean, index: number) => ({ backgroundColor: isHighlighted ? theme.colors.highlight : theme.colors.surface, opacity: index === 0 ? 1 : 0.8, }), })); // In JSX -- call the function with arguments <View style={styles.card(isHighlighted, index)} /> ``` **Why good:** serializable arguments pass to C++ for native recalculation, full TypeScript inference on parameters See [examples/core.md](examples/core.md) for dynamic function patterns and the serializable constraint. --- ### Pattern 4: Breakpoints and Media Queries Define breakpoints in `StyleSheet.configure`, then use breakpoint objects or the `mq` utility in styles. At least one breakpoint must start at `0`. ```typescript // In style definitions -- breakpoint object syntax const styles = StyleSheet.create((theme) => ({ container: { flexDirection: { xs: "column", md: "row", }, padding: { xs: 8, sm: 16, lg: 24, }, }, })); ``` ```typescript // Media query syntax for precise ranges import { mq } from "react-native-unistyles"; const styles = StyleSheet.create(() => ({ sidebar: { display: { [mq.only.width(0, 768)]: "none", [mq.only.width(768)]: "flex", }, }, })); ``` **Why good:** breakpoints cascade like CSS (xs applies until sm overrides), mq provides precise range control, web automatically generates CSS media queries See [examples/responsive.md](examples/responsive.md) for Display/Hide components, landscape/portrait breakpoints, and mixing breakpoints with mq. --- ### Pattern 5: Style Merging (Array Syntax) Never spread Unistyles objects. Spreading destroys the C++ state that tracks dependencies. Use React Native's array syntax for merging. ```typescript // CORRECT -- array syntax preserves C++ state <View style={[styles.container, styles.overlay]} /> <View style={[styles.card, isFocused && styles.focused]} /> // WRONG -- spreading destroys C++ state <View style={{ ...styles.container, ...styles.overlay }} /> ``` **Why bad (spread):** spreading removes the C++ state Unistyles attaches, forcing it to restore state in unpredictable order; triggers dev-mode warnings See [examples/core.md](examples/core.md) for merging patterns and conditional style application. --- ### Pattern 6: withUnistyles for Third-Party Components Third-party components that don't expose native views via `ref` cannot benefit from Unistyles' Shadow Tree updates. Wrap them with `withUnistyles` to subscribe to theme and runtime changes. ```typescript import { withUnistyles } from "react-native-unistyles"; import { BlurHash } from "react-native-blurhash"; // Static mappings -- re-renders only when dependencies change const UniBlurHash = withUnistyles(BlurHash, (theme) => ({ color: theme.colors.tint, })); // Dynamic mappings via uniProps <UniBlurHash uniProps={(theme, rt) => ({ color: rt.colorScheme === "dark" ? theme.colors.darkTint : theme.colors.lightTint, })} /> ``` **Why good:** component re-renders only when its dependencies change, not on every theme/runtime update **When to use:** only for third-party components that don't work with standard Unistyles styles. Regular React Native components (`View`, `Text`, `Pressable`) work without it. See [examples/core.md](examples/core.md) for uniProps priority and when to choose withUnistyles vs useUnistyles. </patterns> --- <decision_framework> ## Decision Framework ### Choosing the Right Styling Approach ``` Does the component need theme colors or runtime values? |-- NO -> Plain StyleSheet.create (static object, no callback) +-- YES -> StyleSheet.create((theme, rt) => ...) | Does it also need component-local values (props, state)? |-- YES -> Dynamic function: style: (arg) => ({ ... }) +-- NO -> Static theme/runtime access is enough Does the style have multiple visual variants (size, color, state)? |-- YES -> Use variants {} inside the style | | | Do combinations of variants need special treatment? | +-- YES -> Add compoundVariants [] +-- NO -> Regular style properties Is this a third-party component that doesn't work with Unistyles styles? |-- Try withUnistyles first (no re-renders) +-- Only if that fails -> useUnistyles hook (causes re-renders) ``` ### Responsive: Breakpoints vs Media Queries vs Display/Hide | Need | Solution | | ---------------------------- | --------------------------------------------- | | Simple per-breakpoint values | Breakpoint object `{ xs: 8, md: 16 }` | | Precise pixel ranges | `mq.only.width(0, 500)` | | Show/hide entire components | `<Display mq={...}>` / `<Hide mq={...}>` | | Orientation-specific styles | Built-in `portrait` / `landscape` breakpoints | </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Spreading styles `{...styles.a, ...styles.b}` -- destroys C++ state, causes unpredictable style resolution, triggers dev warnings - Using `useUnistyles` in regular components -- forces full re-renders, defeats the zero-render architecture - Calling `StyleSheet.create` before `StyleSheet.configure` -- styles won't have access to themes or breakpoints - Importing `StyleSheet` from `react-native` instead of `react-native-unistyles` -- styles work but lose all Unistyles features (themes, variants, breakpoints) - Passing non-serializable arguments to dynamic functions (functions, components, Promises) -- arguments are passed to C++ via `folly::dynamic`, non-serializable values crash **Medium Priority Issues:** - Setting both `initialTheme` and `adaptiveThemes: true` in configure -- they are mutually exclusive, Unistyles will throw an error - Missing Babel plugin configuration -- without it, dependency detection, ref borrowing, and scoped variants don't work - Using `useUnistyles` at the root level -- subscribes the entire app tree to every theme/runtime change - Defining breakpoints without a `0` value -- at least one breakpoint must be `0` for CSS-like cascading to work **Gotchas & Edge Cases:** - The `bottom` inset is NOT dynamic for keyboard -- use `rt.insets.ime` (input method editor) for keyboard-responsive padding - Babel plugin does NOT support moving functions outside `StyleSheet.create` or reassigning `theme`/`rt` to other variables -- the analysis is scope-bound - `ScopedTheme` does not work correctly above `Suspense` boundaries -- place it inside suspended components - Metro Fast Refresh (HMR) does not propagate child changes to parent `ScopedTheme` components -- requires manual refresh - `withUnistyles` uniProps are lower priority than inline props -- inline props override uniProps, which override global mappings - Boolean variants use string keys `"true"` and `"false"` -- they are distinct from a `default` variant - All themes must share the same TypeScript type -- mismatched theme shapes cause type errors - On web, Unistyles converts theme colors to CSS variables -- theme switching swaps a single class on `<body>`, no JS recomputation - `UnistylesRuntime` getters are non-reactive outside StyleSheet -- use `useUnistyles` or `withUnistyles` for reactive access in components - `StyleSheet.addChangeListener()` (v3.1.0+) is the escape hatch for animation libraries that need runtime update notifications </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST import `StyleSheet` from `react-native-unistyles`, NOT from `react-native` -- the Unistyles version is a superset that enables all features)** **(You MUST call `StyleSheet.configure()` BEFORE any `StyleSheet.create()` -- configure in your entry file before importing components)** **(You MUST use array syntax `[styles.a, styles.b]` for merging styles -- NEVER spread `{...styles.a, ...styles.b}` as it destroys C++ state)** **(You MUST NOT use `useUnistyles` hook in regular components -- it forces full re-renders, defeating Unistyles' zero-render architecture)** **(You MUST pass only serializable arguments to dynamic functions -- strings, numbers, booleans, arrays, objects (no functions or components))** **Failure to follow these rules will cause broken styles, unnecessary re-renders, and runtime crashes from the C++ core.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.