Compile-time utility styling for React Native with both UnoCSS-style attributes and Tailwind-style className utilities.
<View flex="1 row" p="4" bg="blue-500" rounded="xl" />
<View className="flex-row items-center rounded-xl bg-blue-500 p-4" />
<View classNames={clsx('p-4', active && 'bg-blue-500')} mt="2" />Static styles are extracted into StyleSheet.create. Conditional values become small style expressions, and existing inline style values retain the highest precedence.
Version 0.2.0 is the first stable release targeting Expo SDK 54: React Native 0.81, React 19.1, and Node.js 20.19.4 or newer. The package peer range is intentionally limited to React Native >=0.81 <0.82 so a future native style-surface change cannot pass unnoticed.
- UnoCSS-style attributify props and Tailwind-style
classNameutilities compile intoStyleSheet.create. - All 153 properties in the React Native 0.81
ViewStyle | TextStyle | ImageStylesurface have direct kebab-case attributes. - Theme tokens, built-in palette overrides, custom themes, and stacked theme/platform variants are supported.
- Static conditionals, arrays, object maps, template literals,
clsx, andclassnamescompile without a runtime utility parser. - Expo SDK 54 demo exports are validated for Web, Android, and iOS.
npm install react-native-attributifyCreate attributify.config.js in the project root:
const { nativePreset } = require("react-native-attributify/presets");
module.exports = {
presets: [nativePreset()],
prefix: "app_",
};Add the Babel plugin:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["react-native-attributify/babel"],
};
};Load the React Native prop augmentations once in an application declaration file:
// attributify.d.ts
import "react-native-attributify/presets/native";The smaller layout-only preset is also available:
const { layoutPreset } = require("react-native-attributify/presets");
module.exports = { presets: [layoutPreset()] };Values follow UnoCSS attributify grouping. Tailwind spacing uses a four-point base, so p="4" produces padding: 16.
<View
flex="1 row center"
p="x-4 y-2"
bg="blue-500"
border="1 solid gray-200"
rounded="lg"
>
<Text text="xl white center bold">Hello</Text>
</View>Long names and the optional UnoCSS conflict prefix are supported:
<View
un-p="4"
padding-horizontal="3"
background="white"
border-color="gray-200"
/>Valueless utilities work in JSX too:
<View flex-row items-center font-bold />className uses normal Tailwind utility syntax. classNames is an equivalent alias for teams that want to avoid conflicts with another transform.
<View className="flex-row items-center gap-3 rounded-xl bg-white p-4 shadow-lg" />
<Text classNames="text-xl font-semibold text-blue-600" />Both forms can be mixed. JSX source order decides conflicts, and explicit style is applied last:
<View className="p-2" p="4" style={{ padding: 24 }} />The compiler understands the common ways React users compose classes:
<View className={active ? 'bg-blue-500' : 'bg-gray-200'} />
<View className={clsx('p-4', active && 'shadow-lg')} />
<View className={classnames({ 'opacity-50': disabled, 'opacity-100': !disabled })} />
<View className={['p-4', active && 'bg-blue-500']} />
<View className={`p-4 ${active ? 'bg-blue-500' : 'bg-gray-200'}`} />
<View bg={active ? 'blue-500' : 'gray-200'} />
<View p={spacing} bg={backgroundColor} />Supported composition forms include:
- string and numeric JSX literals;
- ternaries and
&&expressions; - arrays and nested arrays;
- object syntax used by
clsxandclassnames; - imported aliases such as
import cx from 'clsx'; - template literals containing complete utility names;
- constant bindings;
- static and dynamic variant maps such as
variants[kind]; clsx,classnames,classNames, andcnby default.
Complete class names must exist in source code. This matches Tailwind's static-detection rule:
// Supported
const colors = { success: 'bg-green-500', error: 'bg-red-500' }
<View className={colors[state]} />
// Intentionally preserved, not guessed by the compiler
<View className={`bg-${color}-500`} />Unknown or partially dynamic class expressions are left on the component instead of being silently discarded.
Define any number of themes:
const { nativePreset } = require("react-native-attributify/presets");
module.exports = {
presets: [nativePreset()],
defaultTheme: "light",
themes: {
light: {
colors: { primary: "#2563eb", surface: "#ffffff", text: "#111827" },
},
dark: {
colors: { primary: "#60a5fa", surface: "#111827", text: "#f9fafb" },
},
brand: {
colors: {
primary: "#ff5a1f",
surface: "#fff7ed",
text: "#431407",
// Built-in palette keys can be overridden too.
"blue-500": "#ff5a1f",
},
},
},
};Wrap the application above components that use theme styles:
import { ThemeProvider } from "react-native-attributify";
export function Root() {
return (
<ThemeProvider defaultTheme="light" themes={["light", "dark", "brand"]}>
<App />
</ThemeProvider>
);
}Theme tokens switch automatically when they exist in multiple configured themes:
<View bg="surface" />
<Text className="text-text" />Explicit variants are also supported, including UnoCSS variant groups:
<View bg="white dark:black" />
<View className="bg-white dark:(bg-black text-white)" />
<View className="bg-blue-500 brand:bg-blue-500" />Platform variants can be stacked with theme variants:
<View className="ios:bg-blue-500 android:bg-green-500 native:p-4" />
<View className="dark:ios:bg-black" />Only components that actually contain a theme-dependent style receive a useTheme() call. Existing hook calls, aliases, memo, and forwardRef components are handled without duplicate hooks.
Automatic theme-hook injection targets function components. Theme-dependent props inside class components are preserved unchanged instead of emitting an unbound runtime expression.
The native preset tracks all 153 style properties exposed across ViewStyle, TextStyle, and ImageStyle in React Native 0.81. A test reads the installed React Native declarations, so adding or removing an upstream property breaks the coverage gate instead of silently drifting.
Every property has a direct attributify form made from its kebab-case React Native name:
<View
padding-inline="3"
box-sizing="border-box"
box-shadow="0 2px 8px #0003"
filter="brightness(0.9)"
transform-origin="50% 50%"
/>
<Text
font-variant="small-caps tabular-nums"
text-decoration-line="underline-line-through"
/>
<Image object-fit="scale-down" tint-color="blue-500" />The same names can be used as utilities when their values do not need spaces, for example box-sizing-border-box, user-select-none, and margin-start-3. Spacing and dimensions use the normal four-point scale; bracket values bypass it (padding-inline="[12]" means 12).
Short aliases remain available where they are genuinely useful, including p, m, ms, me, ps, pe, bg, and tint.
The friendly utility set covers React Native-compatible forms of:
- flexbox, gap, alignment, display, overflow, and direction;
- padding, margin, width, height, min/max size, aspect ratio, and inset;
- Tailwind color palettes, arbitrary colors, and color opacity modifiers;
- typography, font weight/family, line height, tracking, and decoration;
- border width/style/color and directional radius;
- position and z-index;
- scale, rotate (including X/Y/Z), perspective, translate, matrix, and skew transforms;
- iOS shadow properties and Android elevation;
- image fit/resize and pointer-event helpers.
CSS-like React Native 0.81 fields such as boxShadow, filter, mixBlendMode, outlines, and experimental background images can be platform- or New-Architecture-dependent. Expo SDK 54 enables the New Architecture by default; the compiler emits the native property, while React Native remains responsible for runtime/platform availability.
Arbitrary values use bracket syntax and bypass the spacing scale:
<View className="w-[120] p-[7] bg-[#316ff6]" />Interactive state variants such as hover: and responsive media variants are not currently compiled because they require a React Native state/media layer. Theme and ios:, android:, web:, windows:, macos:, and native: platform variants are supported by this release.
// Input
<View className="rounded-lg bg-blue-500 p-4" style={customStyle} />
// Shape of generated output
const app_xxx = StyleSheet.create({
app_abc: {
borderRadius: 8,
backgroundColor: '#3b82f6',
padding: 16
}
})
<View style={[app_xxx.app_abc, customStyle]} />The compiler uses a cached Map index for exact rules, a smaller regex list for dynamic rules, memoized utility results, stable style hashing, and a single JSX traversal per file. Identical static styles are emitted once per module.
interface AttributifyConfig {
presets?: Array<Pattern[] | (() => Pattern[])>;
prefix?: string;
themes?: Record<string, ThemeConfig>;
defaultTheme?: string;
classAttributes?: string[];
classFunctions?: string[];
}Defaults:
{
prefix: 'attr_',
presets: [],
themes: {},
defaultTheme: 'default',
classAttributes: ['className', 'classNames'],
classFunctions: ['clsx', 'classnames', 'classNames', 'cn']
}Plugin options can override file configuration, which is useful for tests or monorepo-specific Babel setups.
npm test
npm run build
npm run lintCompiler tests inspect both generated code and the resulting Babel AST.
The example app is an interactive catalog for every supported API group and the complete 153-property React Native 0.81 style surface. It includes pages for syntax, real components, layout, spacing, colors, typography, borders, positioning, transforms, shadows, images, themes, platform variants, conditionals, and direct native properties.
npm run build
npm run demo:install
npm run demo:startThe demo also has standalone validation commands:
npm run demo:typecheck
npm run demo:check-transform
npm run demo:exportMIT
