Installation
Add Aether UI to any React project. Components are copied directly into your codebase — no wrapper library, no lock-in. You own the source. After setup, run npx aether-ui add button to copy any of the 65+ components directly into your project.
Requirements
- ✓Node.js ≥ 18
- ✓React ≥ 18
- ✓TypeScript (strongly recommended — components are typed throughout)
- ✓Tailwind CSS 3.x
Framework guides
Create a new Next.js project (skip if you have one)
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-appMake sure to select TypeScript, Tailwind CSS, and App Router when prompted.
Install Aether UI dependencies
npm install @aetherstack/utils class-variance-authority tailwind-merge clsx
npm install @radix-ui/react-slot @radix-ui/react-dialog @radix-ui/react-tabs
npm install @radix-ui/react-tooltip @radix-ui/react-select @radix-ui/react-checkbox
npm install @radix-ui/react-radio-group @radix-ui/react-switch @radix-ui/react-label
npm install lucide-react tailwindcss-animateOr with pnpm:
pnpm add @aetherstack/utils class-variance-authority tailwind-merge clsx
pnpm add @radix-ui/react-slot @radix-ui/react-dialog @radix-ui/react-tabs
pnpm add @radix-ui/react-tooltip @radix-ui/react-select @radix-ui/react-checkbox
pnpm add @radix-ui/react-radio-group @radix-ui/react-switch @radix-ui/react-label
pnpm add lucide-react tailwindcss-animateConfigure tailwind.config.ts
import type { Config } from "tailwindcss"
import { fontFamily } from "tailwindcss/defaultTheme"
import animatePlugin from "tailwindcss-animate"
const config: Config = {
darkMode: ["class"],
content: [
"./src/app/**/*.{ts,tsx}",
"./src/components/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
},
},
},
plugins: [animatePlugin],
}
export default configAdd CSS variables to globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 274 66% 19%;
--card: 0 0% 100%;
--card-foreground: 274 66% 19%;
--popover: 0 0% 100%;
--popover-foreground: 274 66% 19%;
--primary: 269 74% 57%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 96%;
--secondary-foreground: 272 57% 32%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--accent: 0 0% 96%;
--accent-foreground: 272 57% 32%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 90%;
--input: 0 0% 90%;
--ring: 269 74% 57%;
--radius: 0.5rem;
}
.dark {
--background: 274 66% 4%;
--foreground: 0 0% 98%;
--card: 272 57% 9%;
--card-foreground: 0 0% 98%;
--popover: 272 57% 9%;
--popover-foreground: 0 0% 98%;
--primary: 267 84% 65%;
--primary-foreground: 274 66% 4%;
--secondary: 0 0% 15%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 15%;
--muted-foreground: 0 0% 64%;
--accent: 0 0% 15%;
--accent-foreground: 0 0% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 274 66% 4%;
--border: 0 0% 15%;
--input: 0 0% 15%;
--ring: 267 84% 65%;
}
}
@layer base {
* { @apply border-border; }
body { @apply bg-background text-foreground antialiased; }
}Set up fonts in layout.tsx
import { Inter, JetBrains_Mono } from "next/font/google"
import "./globals.css"
const fontSans = Inter({ subsets: ["latin"], variable: "--font-sans" })
const fontMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${fontSans.variable} ${fontMono.variable} font-sans`}>
{children}
</body>
</html>
)
}Create the cn utility
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}If using @aetherstack/utils, this is already included — import cn from there instead.
Add your first component
# Copy the Button component into your project
npx aether-ui add buttonThe CLI writes the component source directly to src/components/ui/button.tsx. You own the code — edit it freely.
CLI reference
The aether-ui CLI copies component source files into your project. No runtime dependency on Aether UI — you own the code.
# Initialize a project (creates aether.json config)
npx aether-ui init
# List available components
npx aether-ui list
# Add a single component
npx aether-ui add button
# Add multiple components at once
npx aether-ui add button badge card input
# Add to a specific directory
npx aether-ui add button --cwd /path/to/your/projectaether.json
Running aether-ui init creates a config file that tells the CLI where to install components and how your project is structured:
{
"$schema": "https://aether-ui.dev/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}Dark mode
Aether UI uses the class strategy for dark mode — add the dark class to the html element to activate dark mode.
// With next-themes (recommended for Next.js)
npm install next-themesimport { ThemeProvider } from "next-themes"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}For Vite/Remix/Astro, manually toggle the dark class on document.documentElementor use your framework's preferred theme management library.
TypeScript
All components are fully typed. Props extend their corresponding HTML element types so standard HTML attributes (like aria-*, data-*, onClick, etc.) work as expected without any extra casting.
// All HTML button attributes are available
<Button
type="submit"
aria-label="Submit form"
data-testid="submit-btn"
onClick={(e) => handleSubmit(e.currentTarget)}
>
Submit
</Button>
// TypeScript will catch invalid prop combinations
<Button variant="invalid" />
// ^ Type '"invalid"' is not assignable to type 'ButtonVariant'