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.

Next.js App RouterNext.js Pages RouterVite + React💿Remix🚀Astro🔷TanStack Start

Requirements

  • Node.js ≥ 18
  • React ≥ 18
  • TypeScript (strongly recommended — components are typed throughout)
  • Tailwind CSS 3.x

Framework guides

1

Create a new Next.js project (skip if you have one)

terminal
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app

Make sure to select TypeScript, Tailwind CSS, and App Router when prompted.

2

Install Aether UI dependencies

terminal
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-animate

Or with pnpm:

terminal
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-animate
3

Configure tailwind.config.ts

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 config
4

Add CSS variables to globals.css

src/app/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; }
}
5

Set up fonts in layout.tsx

src/app/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>
  )
}
6

Create the cn utility

src/lib/utils.ts
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.

7

Add your first component

terminal
# Copy the Button component into your project
npx aether-ui add button

The 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.

terminal
# 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/project

aether.json

Running aether-ui init creates a config file that tells the CLI where to install components and how your project is structured:

aether.json
{
  "$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.

terminal
// With next-themes (recommended for Next.js)
npm install next-themes
src/app/layout.tsx
import { 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.

example.tsx
// 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'