Docs · Getting started
Installation
Set up a new Next.js app with the same stack, step by step.
This sets up a new Next.js app with everything the components use: Tailwind CSS, motion, Hugeicons, the Sen font, the Arise UI tokens and the theme providers. It takes about ten minutes.
Requirements
- Node.js 20.9 or later. Check with
node -v. - A package manager. Arise UI uses pnpm 10; run
corepack enable pnpmto get it. npm, yarn and bun work too. - macOS, Windows (including WSL) or Linux.
Set up the app
Create the Next.js app
pnpm create next-app@latest my-appChoose No, customize settings and answer the prompts like this:
Prompt Answer TypeScript Yes Linter Biome React Compiler Yes Tailwind CSS Yes src/ directory Yes App Router Yes Cache Components Yes Import alias @/*AGENTS.md Yes That installs Next.js, React, TypeScript, Tailwind CSS and Biome. Then
cd my-app.Add the libraries
Animation, icons and the font:
pnpm add motion @hugeicons/react @hugeicons/core-free-icons @fontsource-variable/senOnly if you'll use the 3D components:
pnpm add three @react-three/fiberpnpm add -D @types/threeConfigure Next.js
Make
next.config.tsmatch this. The Next.js guide explains each option.next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true, reactCompiler: true, experimental: { // Hugeicons ships ~15k named exports from one entry point. // This makes Next load only the icons we actually import. optimizePackageImports: ["@hugeicons/core-free-icons"], }, turbopack: { rules: { "*.css": { loaders: ["@tailwindcss/turbopack"], as: "*.css", }, }, },}; export default nextConfig;Add the design tokens
Replace
src/app/globals.csswith the Arise UI tokens. They turn off Tailwind's default palette and define every color, type size, radius, shadow and easing the components use, for light and dark.src/app/globals.css@import "tailwindcss"; /* * Arise UI Design System — tokens from design.md * * Every design.md CSS variable (--color-primary, --space-*, --radius-*, --shadow-*) * is defined here with its exact name, so code can use either the variable * (var(--color-primary)) or the matching Tailwind utility (bg-primary). * * Theme: <html data-theme="light" | "dark">. A small script in layout.tsx sets it * before first paint from the saved preference (light, dark or system). * * Custom themes (src/lib/theme.ts) override these variables from an unlayered * <style id="arise-custom-theme">, so they win over everything here. */ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); @theme { /* Use only Arise UI tokens — Tailwind's default palette is switched off. */ --color-*: initial; --color-white: #ffffff; --color-black: #000000; /* Primary */ --color-primary: #4f27c0; --color-primary-hover: #3e1d9a; --color-primary-active: #321675; --color-primary-subtle: #f0ebfc; --color-primary-border: #d6c8f5; --color-on-primary: #ffffff; /* Secondary and accent: supporting brand colors, same six steps as primary */ --color-secondary: #0f766e; --color-secondary-hover: #10615a; --color-secondary-active: #154e49; --color-secondary-subtle: #dff3f0; --color-secondary-border: #abdcd6; --color-on-secondary: #ffffff; --color-accent: #be185d; --color-accent-hover: #9d174d; --color-accent-active: #7e1b3f; --color-accent-subtle: #fee8ec; --color-accent-border: #f7c0cc; --color-on-accent: #ffffff; /* Surfaces */ --color-background: #f8f7fc; --color-surface: #ffffff; --color-surface-elevated: #ffffff; --color-surface-muted: #f0eef5; /* Borders */ --color-border: #e4dfec; --color-border-strong: #c9c1d8; /* Text */ --color-text-primary: #211a2e; --color-text-secondary: #625a70; --color-text-muted: #827a90; /* Semantic */ --color-success: #15803d; --color-success-bg: #dcfce7; --color-warning: #a16207; --color-warning-bg: #fef3c7; --color-error: #b91c1c; --color-error-bg: #fee2e2; --color-info: #1d4ed8; --color-info-bg: #dbeafe; /* Inverse surface for tooltips */ --color-inverse: #211a2e; --color-on-inverse: #f5f2fa; /* Code surface: stays dark in both themes */ --color-code: #211a2e; --color-on-code: #f5f2fa; /* Overlay scrim behind dialogs, drawers and sheets */ --color-scrim: rgb(16 14 23 / 0.42); /* Typography — Sen only (design.md §3) */ --font-sans: var(--font-sen); --font-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; --text-*: initial; --text-display: 3rem; --text-display--line-height: 1.1; --text-display--font-weight: 700; --text-h1: 2.25rem; --text-h1--line-height: 1.2; --text-h1--font-weight: 700; --text-h2: 1.875rem; --text-h2--line-height: 1.25; --text-h2--font-weight: 600; --text-h3: 1.5rem; --text-h3--line-height: 1.3; --text-h3--font-weight: 600; --text-h4: 1.25rem; --text-h4--line-height: 1.4; --text-h4--font-weight: 600; --text-body-lg: 1.125rem; --text-body-lg--line-height: 1.6; --text-body: 1rem; --text-body--line-height: 1.5; --text-body-sm: 0.875rem; --text-body-sm--line-height: 1.5; --text-caption: 0.75rem; --text-caption--line-height: 1.4; --text-control: 0.875rem; --text-control--line-height: 1.4; --text-control--font-weight: 600; /* Radius (design.md §5) */ --radius-*: initial; --radius-xs: 4px; --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 24px; --radius-full: 9999px; /* Shadows (design.md §5) — dark values are set under [data-theme="dark"] */ --shadow-*: initial; --shadow-sm: 0 1px 2px rgb(30 20 50 / 0.06); --shadow-md: 0 4px 12px rgb(30 20 50 / 0.08); --shadow-lg: 0 12px 32px rgb(30 20 50 / 0.12); /* Easing — matches src/lib/motion.ts */ --ease-*: initial; --ease-standard: cubic-bezier(0.2, 0, 0, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1); /* Looping CSS animations (cheap, no JS) */ --animate-shimmer: shimmer 1.4s linear infinite; --animate-marquee: marquee var(--marquee-duration, 30s) linear infinite; --animate-float: float 3.2s var(--ease-in-out) infinite; --animate-indeterminate: indeterminate 1.5s var(--ease-in-out) infinite; --animate-stripes: stripes 0.8s linear infinite; --animate-dot: dot-bounce 1.1s var(--ease-in-out) infinite; --animate-bar: bar-pulse 0.9s var(--ease-in-out) infinite; --animate-halo: halo 2.4s var(--ease-out) infinite; @keyframes shimmer { from { background-position: 150% 0; } to { background-position: -50% 0; } } @keyframes marquee { to { transform: translateX(-50%); } } /* A ring that grows and fades from a node, e.g. the Beam hub. */ @keyframes halo { from { transform: scale(1); opacity: 0.45; } to { transform: scale(1.6); opacity: 0; } } @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } @keyframes indeterminate { 0% { transform: translateX(-100%) scaleX(0.4); } 50% { transform: translateX(30%) scaleX(0.6); } 100% { transform: translateX(110%) scaleX(0.4); } } @keyframes stripes { to { background-position: 28px 0; } } @keyframes dot-bounce { 0%, 80%, 100% { transform: translateY(0); opacity: 0.35; } 40% { transform: translateY(-6px); opacity: 1; } } @keyframes bar-pulse { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }} /* Friendlier names for the three text colors: text-fg, text-fg-secondary, text-fg-muted */@theme inline { --color-fg: var(--color-text-primary); --color-fg-secondary: var(--color-text-secondary); --color-fg-muted: var(--color-text-muted);} @layer base { :root { color-scheme: light; /* Spacing (design.md §4). Tailwind's 4px scale matches: p-4 = --space-4 = 16px. */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px; --space-12: 48px; --space-16: 64px; /* Motion durations — matches src/lib/motion.ts */ --motion-instant: 100ms; --motion-fast: 150ms; --motion-base: 200ms; --motion-moderate: 300ms; --motion-slow: 450ms; --motion-slower: 700ms; } :root[data-theme="dark"] { color-scheme: dark; --color-primary: #a78bfa; --color-primary-hover: #c4b5fd; --color-primary-active: #ddd6fe; --color-primary-subtle: #2b2043; --color-primary-border: #57417f; --color-on-primary: #1c1233; --color-secondary: #6cc3ba; --color-secondary-hover: #a9ded7; --color-secondary-active: #d3efeb; --color-secondary-subtle: #152d2a; --color-secondary-border: #325753; --color-on-secondary: #0b1d1b; --color-accent: #ff8dab; --color-accent-hover: #ffc3d0; --color-accent-active: #ffdee5; --color-accent-subtle: #411824; --color-accent-border: #7a3549; --color-on-accent: #2e0c17; --color-background: #100e17; --color-surface: #191620; --color-surface-elevated: #211d2b; --color-surface-muted: #282431; --color-border: #37313f; --color-border-strong: #51485e; --color-text-primary: #f5f2fa; --color-text-secondary: #b9b1c7; --color-text-muted: #92899f; --color-success: #4ade80; --color-success-bg: #153423; --color-warning: #facc15; --color-warning-bg: #3b2c12; --color-error: #f87171; --color-error-bg: #3b1d22; --color-info: #60a5fa; --color-info-bg: #172b45; --color-inverse: #f5f2fa; --color-on-inverse: #211a2e; --color-scrim: rgb(0 0 0 / 0.6); --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.25); --shadow-md: 0 4px 12px rgb(0 0 0 / 0.35); --shadow-lg: 0 12px 32px rgb(0 0 0 / 0.45); } html { -webkit-text-size-adjust: 100%; } body { background-color: var(--color-background); color: var(--color-text-primary); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } button:not(:disabled), [role="button"]:not([aria-disabled="true"]), label[for], summary { cursor: pointer; } /* design.md §6: 2px primary outline with 2px offset on keyboard focus */ :focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } ::selection { background-color: var(--color-primary-subtle); color: var(--color-text-primary); } /* Theme switch uses a View Transition (see theme-provider.tsx). */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } /* biome-ignore-start lint/complexity/noImportantStyles: reduced-motion must beat every component animation, including utilities. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* biome-ignore-end lint/complexity/noImportantStyles: end of reduced-motion block */} @utility scrollbar-none { scrollbar-width: none; &::-webkit-scrollbar { display: none; }}Add the shared helpers
Most components import these. Create each file at the path in its title.
src/lib/cn.tstype ClassValue = string | false | null | undefined | 0; /** Join class names, skipping falsy values. Later classes win in the cascade only by order. */export function cn(...classes: ClassValue[]): string { return classes.filter(Boolean).join(" ");}src/lib/motion.tsimport type { Transition, Variants } from "motion/react"; /** * Arise UI motion tokens. * * The same values exist as CSS variables in globals.css (--motion-*, --ease-*), * so CSS transitions and motion/react animations stay in sync. * * Rule of thumb: * - Hover, focus, color changes → CSS transitions with these tokens * - Enter/exit, layout, gestures, springs → motion/react with these tokens */ /** Durations in seconds (motion/react uses seconds). */export const duration = { /** 100ms — hover tints, list highlights */ instant: 0.1, /** 150ms — buttons, focus rings, small fades */ fast: 0.15, /** 200ms — menus, tooltips, toggles */ base: 0.2, /** 300ms — tabs, accordions, popovers */ moderate: 0.3, /** 450ms — drawers, carousels, sheets */ slow: 0.45, /** 700ms — charts, page-level entrances */ slower: 0.7,} as const; type Bezier = [number, number, number, number]; /** Cubic-bezier curves. */export const ease = { /** Most property changes */ standard: [0.2, 0, 0, 1] as Bezier, /** Things entering the screen */ out: [0.16, 1, 0.3, 1] as Bezier, /** Things leaving the screen */ in: [0.4, 0, 1, 1] as Bezier, /** Moving between two places */ inOut: [0.65, 0, 0.35, 1] as Bezier, /** Small overshoot for playful pops (CSS equivalent of a light spring) */ overshoot: [0.34, 1.56, 0.64, 1] as Bezier,}; /** Physics springs for interactive and layout motion. */export const spring = { /** Indicators, thumbs, segmented controls */ snappy: { type: "spring", stiffness: 520, damping: 38, mass: 0.8 }, /** Panels, cards, layout shifts */ gentle: { type: "spring", stiffness: 260, damping: 28 }, /** Likes, checkmarks, celebratory pops */ bouncy: { type: "spring", stiffness: 480, damping: 16 }, /** Draggable sheets */ sheet: { type: "spring", stiffness: 380, damping: 36 },} satisfies Record<string, Transition>; /** Ready-made transitions. */export const transition = { fast: { duration: duration.fast, ease: ease.standard }, base: { duration: duration.base, ease: ease.standard }, enter: { duration: duration.base, ease: ease.out }, exit: { duration: duration.fast, ease: ease.in }, moderate: { duration: duration.moderate, ease: ease.out }, slow: { duration: duration.slow, ease: ease.out }, move: { duration: duration.moderate, ease: ease.inOut },} satisfies Record<string, Transition>; /** Shared variants for common enter/exit patterns. */export const fadeUp: Variants = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0, transition: transition.moderate }, exit: { opacity: 0, y: 6, transition: transition.exit },}; export const scaleIn: Variants = { hidden: { opacity: 0, scale: 0.96 }, visible: { opacity: 1, scale: 1, transition: transition.enter }, exit: { opacity: 0, scale: 0.96, transition: transition.exit },}; /** Parent variant that staggers its children. */export function staggerChildren(step = 0.05, delayChildren = 0): Variants { return { hidden: {}, visible: { transition: { staggerChildren: step, delayChildren } }, };}src/components/ui/icon.tsximport { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; const SIZES = { xs: 14, sm: 16, md: 20, lg: 24, xl: 32 } as const; export type IconSize = keyof typeof SIZES; export type IconProps = { icon: IconSvgElement; /** sm 16 · md 20 (default) · lg 24, per design.md §7 */ size?: IconSize | number; /** Set for meaningful icons. Omit for decorative icons next to visible text. */ label?: string; className?: string; strokeWidth?: number;}; /** * Hugeicons wrapper. Icons draw with currentColor, so set color on the parent * (e.g. `text-fg-muted`) instead of on the icon. */export function Icon({ icon, size = "md", label, className, strokeWidth = 1.5,}: IconProps) { const px = typeof size === "number" ? size : SIZES[size]; return ( <HugeiconsIcon icon={icon} size={px} strokeWidth={strokeWidth} color="currentColor" className={className ? `shrink-0 ${className}` : "shrink-0"} aria-hidden={label ? undefined : true} aria-label={label} role={label ? "img" : undefined} /> );} export type { IconSvgElement };For theming, also copy
src/lib/theme.ts,theme-provider.tsx,custom-theme.tsxandtoast.tsx. Open any component's Code view to copy its source; Using components shows how.Add the providers and layout
src/app/providers.tsx"use client"; import { MotionConfig } from "motion/react";import type { ReactNode } from "react";import { CustomThemeProvider } from "@/components/ui/custom-theme";import { ThemeProvider } from "@/components/ui/theme-provider";import { ToastProvider } from "@/components/ui/toast";import { transition } from "@/lib/motion"; /** * App-wide client providers. * - CustomThemeProvider: the user's own colors, radius and font (see lib/theme.ts). * - MotionConfig reducedMotion="user": motion/react skips transform/layout animations * when the OS asks for reduced motion (opacity and color still animate). */export function Providers({ children }: { children: ReactNode }) { return ( <ThemeProvider> <CustomThemeProvider> <MotionConfig reducedMotion="user" transition={transition.base}> <ToastProvider>{children}</ToastProvider> </MotionConfig> </CustomThemeProvider> </ThemeProvider> );}src/app/layout.tsximport type { Metadata } from "next";import localFont from "next/font/local";import { customThemeScript, themeScript } from "@/lib/theme";import { Providers } from "./providers";import "./globals.css"; // Sen, self-hosted: builds never need to reach Google Fonts.const sen = localFont({ src: "../../node_modules/@fontsource-variable/sen/files/sen-latin-wght-normal.woff2", variable: "--font-sen", weight: "400 800", display: "swap",}); export const metadata: Metadata = { title: "My app" }; /** Light/dark preference, then any custom theme, applied before first paint. */const HEAD_SCRIPT = themeScript + customThemeScript; export default function RootLayout({ children }: LayoutProps<"/">) { return ( <html lang="en" className={sen.variable} data-theme="light" suppressHydrationWarning> <head> {/* biome-ignore lint/security/noDangerouslySetInnerHtml: static script, no user input. */} <script dangerouslySetInnerHTML={{ __html: HEAD_SCRIPT }} /> </head> <body> <Providers>{children}</Providers> </body> </html> );}Run it
pnpm devOpen
http://localhost:3000. Editsrc/app/page.tsxand the page reloads as you save.
Scripts
| Script | What it does |
|---|---|
dev | Development server with Turbopack |
build | Production build; static pages prerender |
start | Serves the production build |
lint | biome check: lint, format check, imports |
format | biome format --write |
typecheck | tsc --noEmit |
create-next-app adds the first four. Add the last two to package.json yourself:
"scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "biome check", "format": "biome format --write", "typecheck": "tsc --noEmit"}