unative
v0.0.20
Published
One UI Library for Web and Mobile
Readme
Unative: One UI Library for Web and Mobile
⚠️ Important: This project is still under development. Do not use this library in your production projects. You can start using it once the first stable version is launched.
Example Project
For a complete example of how to use Unative with Expo, check out the Unative Expo example project. It demonstrates how to set up Unative with Expo Router, Nativewind, TypeScript, and more.

Table of Contents
- What is Unative?
- The Story Behind
- Learn once, Use everywhere
- Optimized for Both Platforms
- ToDo's
- Installation
- Your first component
- Contributing
- License
Introduction
What is Unative?
Unative makes it easy to create great-looking and consistent user interfaces for both React Native and web apps. Powered by Tailwind CSS and Nativewind, it gives you a fast and simple way to handle styling across platforms. With full support for React Server Components, Unative helps you use modern tools to build amazing projects effortlessly.
The Story Behind
As someone who has been working as a Frontend Developer for years, I’ve always noticed a significant gap between UI libraries for web and mobile. I needed a UI library that was fully optimized for both platforms—a library that I could use seamlessly on the web (without requiring any extra layers like react-native-web) and on mobile without being restricted by React Native’s limitations for web elements.
Most importantly, I wanted something that could stay in sync across all my projects, ensuring consistency and efficiency in my workflow.
Learn once, Use everywhere
The Unative UI Library is inspired by ShadCN, with additional support from RNR and RN-Primitives. It is designed to work seamlessly across both web and mobile platforms, offering a truly native experience.
Unative does not require react-native-web in nextjs or other web frameworks. No components from React Native will be loaded in your web projects, even when using frameworks like Next.js, Vite, or any other React-based framework. This means you get an optimized, fully native experience for web and mobile without relying on compatibility layers or workarounds.
Whether you're building a mobile app or a web project, Unative provides a single, unified library that ensures consistency, performance, and simplicity across platforms.
// The only file you need
// Nextjs, React Native, React Native Web, Expo and ...
import { cn } from "unative/lib/utils";
import type { BoxProps } from "unative/ui/box";
import { Box } from "unative/ui/box";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "unative/ui/card";
import { Text } from "unative/ui/text";
export type MyCardProps = BoxProps & {
title: string;
description?: string;
content: string;
};
export const MyCard = ({
title,
content,
className,
...props
}: MyCardProps) => {
return (
<Card className={cn("flex", className)} {...props}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>My Description</CardDescription>
</CardHeader>
<CardContent>
<Box>
<Text>{content}</Text>
</Box>
</CardContent>
</Card>
);
};Optimized for Both Platforms
Unative is fully optimized for both web and mobile platforms. Everything is loaded in a completely treeshakeable way. For example, if you only use the Box component on the web, the size of the import is as minimal as importing a simple <div>. On React Native, it’s equivalent to importing a View from react-native.
All components are processed individually, ensuring maximum efficiency. Each core component consists of two main files:
- web: Includes all dependencies required for running on the web, powered by Radix Primitives.
- native: Includes all dependencies for running on React Native and React Native Web.
This modular approach ensures that your projects remain lightweight and optimized, regardless of the platform you’re building for.
ToDo's
Infrastructure
- [x] Theming
- [x] Separate native and web files with same folder structure
- [x] Auto generate css theme variables by adding HOC to metro.config.js and next.config.js
Core Elements
- [x] Box
- [x] Text
- [x] Button
- [x] Typography
- [x] Separator
- [x] Avatar
- [x] Badge
- [x] Card
Components
- [x] Accordion
- [x] Alert
- [x] Alert Dialog
- [x] Aspect Ratio
- [x] Avatar
- [x] Badge
- [x] Box
- [x] Button
- [x] Card
- [x] Checkbox
- [x] Collapsible
- [x] Context Menu
- [x] Dialog
- [x] Dropdown Menu
- [x] Hover Card
- [x] Menu Bar
- [x] Navigation Menu
- [x] Popover
- [x] Progress
- [x] Separator
- [x] Skeleton
- [x] Table
- [x] Tabs
- [x] Text
- [x] Toggle
- [x] Toggle Group
- [x] Tooltip
- [x] Typography
- [ ] ActionSheet
- [ ] Toast
- [ ] BottomSheet
Forms & Inputs
- [ ] Form
- [x] Label
- [x] TextInput
- [x] TextArea
- [x] Checkbox
- [x] Radio Group
- [ ] Date Input
- [x] Select
- [ ] Select Async
- [ ] OTP Input
- [x] Switch
- [ ] SegmentedControl
Blocks
- [ ] Theme Switch
- [ ] Auth (Login/Register)
- [ ] Onboarding
- [ ] Calendar
Installation
Installation Guide for Mobile
1. Create Your App with Expo
Start by creating your mobile app using Expo.
npx create-expo-app my-app2. Add NativeWind
Install NativeWind and ensure it works correctly in your project.
pnpm i nativewindFollow the setup guide in the NativeWind documentation.
3. Install Unative
Finally, add the Unative library to your project:
npx expo install unativePeer Dependencies
npx expo install clsx tailwind-merge class-variance-authority @react-native-async-storage/async-storage react-native-reanimated react-native-safe-area-contextMetro
// metro.config.js
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const { withUnative } = require("unative/with-unative");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = withUnative(
withNativeWind(config, { input: "./global.css" }),
{
css: "./global.css",
outputDir: "./src/lib/unative",
}
);Tailwind Config
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./node_modules/unative/**/*.{js,jsx,mjs,ts,tsx}",
],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
positive: {
DEFAULT: "hsl(var(--positive))",
foreground: "hsl(var(--positive-foreground))",
},
warning: {
DEFAULT: "hsl(var(--warning))",
foreground: "hsl(var(--warning-foreground))",
},
link: "hsl(var(--link))",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
},
},
plugins: [],
};Installation Guide for NextJS
Step 1: Set Up TailwindCSS
Make sure TailwindCSS is installed and configured in your Next.js project. If you haven’t done this yet, follow the official TailwindCSS installation guide for Next.js.
pnpm i tailwindcss postcss autoprefixer
npx tailwindcss init -p2. Install Unative
pnpm i unativepnpm i clsx tailwind-merge class-variance-authority next-themes lucide-react3. Tailwind Configuration
// tailwind.config.ts
import type { Config } from "tailwindcss";
export default {
darkMode: "class",
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/unative/**/*.{js,jsx,ts,tsx}",
"./node_modules/@unative/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
borderRadius: {
none: "0",
DEFAULT: "var(--radius)",
button: "var(--radius-button)",
card: "var(--radius-card)",
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
positive: {
DEFAULT: "hsl(var(--positive))",
foreground: "hsl(var(--positive-foreground))",
},
warning: {
DEFAULT: "hsl(var(--warning))",
foreground: "hsl(var(--warning-foreground))",
},
link: "hsl(var(--link))",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
1: "hsl(var(--chart-1))",
2: "hsl(var(--chart-2))",
3: "hsl(var(--chart-3))",
4: "hsl(var(--chart-4))",
5: "hsl(var(--chart-5))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
},
},
plugins: [],
} satisfies Config;
Your first component
import { cn } from "unative/lib/utils";
import type { BoxProps } from "unative/ui/box";
import { Box } from "unative/ui/box";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "unative/ui/card";
import { Text } from "unative/ui/text";
export type MyCardProps = BoxProps & {
title: string;
description?: string;
content: string;
};
export const MyCard = ({
title,
content,
className,
...props
}: MyCardProps) => {
return (
<Card className={cn("flex", className)} {...props}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>My Description</CardDescription>
</CardHeader>
<CardContent>
<Box>
<Text>{content}</Text>
</Box>
</CardContent>
</Card>
);
};Theming
Config HOC
NextJS
// next.config.ts
import type { NextConfig } from "next";
import { withUnative } from "unative/theme/with-unative";
const nextConfig: NextConfig = {
transpilePackages: ["unative"],
};
export default withUnative(nextConfig, {
css: "./src/app/globals.css",
outputDir: "./src/lib/unative",
});Metro
// metro.config.js
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const { withUnative } = require("unative/theme/with-unative");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = withUnative(
withNativeWind(config, { input: "./global.css" }),
{
css: "./global.css",
outputDir: "./src/lib/unative",
}
);Provider
import { Provider as UnativeProvider } from "unative/theme/provider";
import { THEMES } from "@/lib/unative/themes"; // Configured in metro.config.js or next.config.ts
<UnativeProvider themes={THEMES}>{children}</UnativeProvider>;useTheme hook
import { useTheme } from "unative/theme";
const App = () => {
const {
themes, // All Installed Themes
theme, // Current Theme
isDarkMode,
setTheme, // Set theme by theme-name
setScheme, // Set Scheme => dark | light | system
colorScheme, // dark | light
colorSchemes, // [dark, light, system]
isInitialized,
rawThemes, // Raw css theme values - Not optimized for react native
savedColorScheme, // system | light | dark
rawThemeValues,
} = useTheme();
return null;
};Base CSS variables
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 0%;
--card: 0 0% 100%;
--card-foreground: 0 0% 0%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 0%;
--primary: 220 80% 50%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 0%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 90%;
--muted-foreground: 0 0% 30%;
--accent: 220 80% 90%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 80% 60%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 90%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--radius-lg: 2rem;
--radius-button: 6px;
--radius-card: 6px;
--positive: 142 60% 40%;
--positive-foreground: 0 0% 100%;
--warning: 40 100% 50%;
--warning-foreground: 39.6 100% 50.2%;
--link: 216 100% 60%;
}
.dark {
--background: 0 0% 0%;
--foreground: 0 0% 100%;
--card: 0 0% 9.02%;
--card-foreground: 0 0% 100%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 0 0% 100%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 0 0% 100%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 0 0% 100%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 15%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
--radius: 3rem;
--positive: 142 72% 29%;
--positive-foreground: 0 0% 100%;
--warning: 39.6 100% 50.2%;
--warning-foreground: 39.6 100% 50.2%;
--link: 216.89 100% 59.61%;
}
}
.theme-brown {
--background: 31 57% 93%;
--primary: 30 100% 50%;
--primary-foreground: 0 0% 100%;
--card: 31 57% 85%;
--accent: 30 100% 80%;
--radius: 0.5rem;
--radius-lg: 2rem;
--radius-button: 24px;
--radius-card: 0.5rem;
&.dark {
--background: 0 0% 0%;
--primary: 30 100% 50%;
--primary-foreground: 0 0% 100%;
--card: 31 57% 8%;
--accent: 30 100% 30%;
}
}
.theme-third {
--background: 0 0% 100%;
--primary: 16 65% 40%;
--primary-foreground: 0 0% 100%;
&.dark {
--background: 0 0% 0%;
--primary: 16 65% 40%;
--primary-foreground: 0 0% 100%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background font-sans text-foreground antialiased;
}
}Contributing
We’re excited to have you contribute to Unative! Contributions help make this library better for everyone. Whether you’re fixing bugs, improving documentation, or adding new features, we’d love to collaborate with you.
How to Contribute
- Fork the repository and clone it locally.
- Install the dependencies using
pnpm:pnpm install - Create a new branch for your changes:
git checkout -b feature/my-awesome-feature - Make your changes and test them thoroughly.
- Commit your changes with a meaningful commit message:
git commit -m "Add: My awesome feature" - Push your branch:
git push origin feature/my-awesome-feature - Open a Pull Request and describe your changes in detail.
Code of Conduct
By contributing, you agree to abide by our Code of Conduct.
License
Unative is licensed under the MIT License. Feel free to use, modify, and distribute the library as per the terms of the license.
