Skip to content

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 pnpm to get it. npm, yarn and bun work too.
  • macOS, Windows (including WSL) or Linux.

Set up the app

  1. Create the Next.js app

    pnpm create next-app@latest my-app

    Choose No, customize settings and answer the prompts like this:

    PromptAnswer
    TypeScriptYes
    LinterBiome
    React CompilerYes
    Tailwind CSSYes
    src/ directoryYes
    App RouterYes
    Cache ComponentsYes
    Import alias@/*
    AGENTS.mdYes

    That installs Next.js, React, TypeScript, Tailwind CSS and Biome. Then cd my-app.

  2. Add the libraries

    Animation, icons and the font:

    pnpm add motion @hugeicons/react @hugeicons/core-free-icons @fontsource-variable/sen

    Only if you'll use the 3D components:

    pnpm add three @react-three/fiber
    pnpm add -D @types/three
  3. Configure Next.js

    Make next.config.ts match this. The Next.js guide explains each option.

    next.config.ts
    import 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;
  4. Add the design tokens

    Replace src/app/globals.css with 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;  }}
  5. Add the shared helpers

    Most components import these. Create each file at the path in its title.

    src/lib/cn.ts
    type 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.ts
    import 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.tsx
    import { 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.tsx and toast.tsx. Open any component's Code view to copy its source; Using components shows how.

  6. 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.tsx
    import 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>  );}
  7. Run it

    pnpm dev

    Open http://localhost:3000. Edit src/app/page.tsx and the page reloads as you save.

Scripts

ScriptWhat it does
devDevelopment server with Turbopack
buildProduction build; static pages prerender
startServes the production build
lintbiome check: lint, format check, imports
formatbiome format --write
typechecktsc --noEmit

create-next-app adds the first four. Add the last two to package.json yourself:

package.json
"scripts": {  "dev": "next dev",  "build": "next build",  "start": "next start",  "lint": "biome check",  "format": "biome format --write",  "typecheck": "tsc --noEmit"}