# Koala UI documentation > A commercial React design system: 123 components built on Tailwind CSS v4, Radix UI and > tailwind-variants, with four themes (light, cream, dark, moonlight), eight accents and a > two-step density axis, all flowing from one layer of semantic CSS variables. The CLI copies the > source into your repo, so you own it; there is no runtime package to be locked into. Every free docs page (guides, foundations and components) as markdown, in sidebar order. The index with the section catalog is https://www.koalaui.com/llms.txt; each page below also lives at its own URL, given above it. ## Conventions - Multi-part components use `tv` with `slots`; there is no `cva` in this codebase. - Parts are NAMED exports, never dot-notation: `Card`, `CardHeader`, `CardBody`. Namespaced statics do not survive the RSC server-to-client boundary. - Cross-part state flows through React Context (`lib/create-context.tsx`), never by cloning children. - Colors, radii, shadows and motion come from tokens. No raw hex, `oklch()`, `cubic-bezier()` or millisecond literals inside a component. - Every component accepts `className`, merged last through `tailwind-merge`. - React 19: `ref` is a regular prop, so there is no `forwardRef`. ## Install and update ```bash npx koalaui-cli@latest init # once: tokens, lib helpers, theme provider, base deps npx koalaui-cli@latest add button card dialog # components, their dependencies pulled along npx koalaui-cli@latest list # everything available, free and Pro npx koalaui-cli@latest diff [item] # what changed upstream since you installed npx koalaui-cli@latest update [item] [--force] # replace untouched files; edited files are kept npx koalaui-cli@latest template [dir] # Pro: start a whole site from a template npx koalaui-cli@latest skill # write the Koala agent skill into .claude/skills/koala-ui ``` `init` writes `koala.css` and imports it from the stylesheet that imports Tailwind. In Next.js that is `app/globals.css`, and the root layout then loads Inter as `--font-sans` and DM Sans as `--font-heading` with `next/font`. In Vite it is `src/index.css` (tokens in `src/styles/koala.css`), and `init` wires both faces itself from `@fontsource-variable`. Either way the app is wrapped in the `ThemeProvider` from `components/theme-provider.tsx` (themes: light, cream, dark, moonlight; `forcedTheme` pins one). Components land in `components/ui//` (under `src/` in Vite) and import through the `@/*` path alias. `add` refuses while a flat shadcn/ui file of the same name would shadow a Koala folder. Every install is recorded in `koala.json`, which is how `diff` and `update` know what you edited. ## Free and Pro - Free, no account: all 123 components, the lib helpers and the design tokens. Their docs, code included, are below. - Pro, with a license: every section, the page examples and the templates. Activate a key once per machine with `npx koalaui-cli@latest login `, then add Pro items like free ones. Licenses: https://www.koalaui.com/pro. A section page's markdown names its Pro variants and stops there: their source is never published in these docs. --- # Koala UI The React implementation of Koala UI, built on Next.js, Tailwind v4, Radix, and tailwind-variants. This is the React side of [koalaui.com](https://koalaui.com): the same design system, as production-ready React components. Components are owned source in your repo, styled with a single `tv` recipe, accessible by way of Radix primitives, and themed entirely through semantic design tokens. Four themes ship out of the box: light, dark, cream, and moonlight. - [Get started](https://www.koalaui.com/docs/installation.md) - [Browse components](https://www.koalaui.com/docs/components/button.md) - [Architecture](https://www.koalaui.com/docs/architecture.md) --- # Installation Add Koala UI to your React app. Run the one-time setup, wire the fonts and theme into your layout, add the components you need, and pull updates later without losing your edits. Koala UI ships **owned source**: the CLI copies each component into your repo under `components/ui/`, so you own and edit the code, no runtime dependency on us. The fastest path is the CLI; you can also copy any component by hand from its docs page. It sets up Next.js and Vite apps alike. ## Quick start 1. **Run the setup** `init` writes the design tokens to `koala.css` and imports them from the stylesheet that imports Tailwind (`app/globals.css` in Next.js, `src/index.css` in Vite), drops the shared helpers (`cn` and the `tv` wrapper) into `lib/`, adds the theme provider and installs the base dependencies. It tells the two frameworks apart on its own. No account or auth needed. ```bash npx ``` On a fresh `create-next-app` project it replaces the starter stylesheet, whose own colors and dark media query would fight Koala’s themes. On an existing stylesheet it only adds the imports, and warns you about anything left that would override the themes: its own `--background`, `--foreground` or `--radius`, or a dark variant of its own. Coming from shadcn/ui? It replaces shadcn’s `lib/utils.ts`, and [the migration guide](https://www.koalaui.com/docs/migrating-from-shadcn.md) covers the rest. 2. **Wire the fonts and the theme** **Next.js** Koala sets UI and body copy in Inter and headings in DM Sans, read through the `--font-sans` and `--font-heading` variables. Load both with `next/font` and wrap your app in the `ThemeProvider` that `init` installed. Until you do, text falls back to the system sans and only the light theme applies. ```tsx import { Inter, DM_Sans } from "next/font/google" import { ThemeProvider } from "@/components/theme-provider" import "./globals.css" const sans = Inter({ subsets: ["latin"], variable: "--font-sans" }) const heading = DM_Sans({ subsets: ["latin"], variable: "--font-heading", axes: ["opsz"] }) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` **Vite** Koala sets UI and body copy in Inter and headings in DM Sans. In a Vite app `init` has already wired both: it installs them from npm and declares `--font-sans` and `--font-heading` in your stylesheet, so the faces ship with your app and need no font CDN (a Tauri or Electron shell with a strict content policy included). ```css @import "tailwindcss"; @import "@fontsource-variable/inter"; @import "@fontsource-variable/dm-sans/opsz.css"; @import "./styles/koala.css"; /* Koala UI faces, wired by `koalaui init`: Inter for UI and body, DM Sans for headings. */ :root { --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; --font-heading: "DM Sans Variable", "Inter Variable", ui-sans-serif, system-ui, sans-serif; } ``` Wrap your app in the `ThemeProvider` that `init` installed, in `src/main.tsx`. Until you do, only the light theme applies. ```tsx import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { ThemeProvider } from "@/components/theme-provider" import App from "./App" import "./index.css" createRoot(document.getElementById("root")!).render( , ) ``` The theme follows the visitor’s OS by default. Switch it anywhere with `useTheme` from next-themes, which works outside Next.js too. An app with a single theme pins it instead: ``. ```tsx "use client" import { useTheme } from "next-themes" import { Button } from "@/components/ui/button" // "light" | "cream" | "dark" | "moonlight" | "system" export function MoonlightButton() { const { setTheme } = useTheme() return } ``` 3. **Add components** Pass one or more component slugs. Each lands in `components/ui//` (under `src/` in a Vite app) with its dependencies pulled along automatically. ```bash npx ``` 4. **Import and use** Import from the path the CLI wrote to. Every component is a named export, themed through tokens, and accepts `className`. ```tsx import { Button } from "@/components/ui/button" export function Example() { return } ``` ## Updating The code is yours, so updates never land on their own. Every install is recorded in `koala.json` at your project root: which version of each item you took, and a fingerprint of every file as it was written. Commit it with the rest of your code. 1. **See what changed** On its own, `diff` lists the items with an update and the files you have edited. Name an item to read the line changes before you take them. ```bash npx ``` 2. **Apply the update** Files you never touched are replaced. Files you edited are kept as they are and listed, so an update never overwrites your work. Any new dependency an update needs comes along with it. ```bash npx ``` To take Koala’s version of a file you edited, merge it by hand from the diff, or run `npx koalaui-cli@latest update --force` to replace your copy outright. `init` is tracked too, so the tokens in `koala.css` update the same way. ## Pro sections Components, the lib helpers and the tokens are free. Every section, the page examples and the templates are **Pro** and need a license key. Activate it once per machine, then add Pro items exactly like free ones. 1. **Activate your license** Lemon Squeezy emails your key with your receipt, and keeps it in [your orders](https://app.lemonsqueezy.com/my-orders). Activating claims one seat and saves the key and the seat to `~/.koalaui/config.json`, readable by you only. A bad key fails here, not later. ```bash npx ``` 2. **Add a Pro item** Run `npx koalaui-cli@latest list` to see everything; Pro items are marked with a lock. ```bash npx ``` `npx koalaui-cli@latest whoami` shows the active license and whether this machine is activated. `npx koalaui-cli@latest logout` releases the seat and forgets the key, so run it before wiping a machine or the seat stays taken. A license covers one person. Your key is exchanged for the source on our server, so you never receive a repository token and no access is granted to your GitHub account. Every Pro file arrives stamped with a fingerprint of your license, never the key itself. To read the source before you install it, unlock the site at [/account](https://www.koalaui.com/account) with your key and purchase email: the Code tab of every Pro section then shows its files. Unlocking the site uses no seat. A few sections link with `next/link`. In a Vite app `add` names them after it writes them: swap that import for your router’s link. ## Templates A template is a whole site: a Next.js project with its pages, its own design layer and the Koala UI components it uses. It is Pro, and it installs into a new, empty folder rather than into a project you already have. ```bash npx ``` The CLI writes the source, downloads the pictures from the live demo and checks each one against its checksum, then runs `npm ci`. Start it with `npm run dev`; its README says where the design variables live and what to replace before you launch. Inside it, `npx koalaui-cli@latest update` keeps the library files current like in any other project. ## CI and agents Each `login` claims a seat, so a CI job must not run one per build. Activate one seat for CI once, from your own machine, without saving it on that machine: ```bash npx ``` It prints `KOALAUI_LICENSE` and `KOALAUI_INSTANCE`. Store both as secrets in your CI and the CLI reads them from the environment. On CI, `login` refuses to run without `--no-save`. Coding agents working on your machine use your saved seat and need nothing extra. ## Manual installation Prefer to copy by hand? Install the shared dependencies, bring the tokens over, then copy any component source straight from its docs page. Hand-copied files are not in `koala.json`, so the CLI’s `update` cannot track them. 1. **Install the dependencies** ```bash npm install clsx tailwind-merge tailwind-variants tw-animate-css next-themes ``` In a Vite app, the two faces as well: ```bash npm install @fontsource-variable/inter @fontsource-variable/dm-sans ``` 2. **Copy the tokens** The whole theme layer, color roles, radius and shadow scales, motion tokens and the custom utilities, lives in one stylesheet. Copy `registry/koala.css` from the repo to `app/koala.css` (`src/styles/koala.css` in Vite) and import it right after Tailwind. Without it, nothing is styled. In Vite, add the font lines from step 2 of the quick start as well. ```css @import "tailwindcss"; @import "./koala.css"; ``` 3. **Wire the fonts and the theme** Copy `components/theme-provider.tsx` from the repo, then set up your root layout (or, in Vite, your entry file) as in step 2 of the quick start. 4. **Copy the source** Paste a component into `components/ui//` and point its imports at your project: components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Requirements - **React 19**: components use `ref` as a regular prop (no `forwardRef`). - **Tailwind CSS v4**: the tokens use `@theme`, `@utility` and `@custom-variant`, which v3 cannot read. A v3 project has to migrate first. - **The `@/*` path alias**: every component imports through it. Most Next.js projects already have it; a Vite app declares it twice, for TypeScript and for Vite. `init` tells you if yours doesn’t. **Next.js** ```ts { "compilerOptions": { "paths": { "@/*": ["./*"] } } } ``` **Vite** ```ts // tsconfig.json and tsconfig.app.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ``` ```ts // vite.config.ts import path from "node:path" import tailwindcss from "@tailwindcss/vite" import react from "@vitejs/plugin-react" import { defineConfig } from "vite" export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, }) ``` - **Next.js or Vite**: `init` recognizes both and wires each its own way. Any other React 19 + Tailwind v4 setup works too: it finds the stylesheet that imports Tailwind, and the fonts follow the Vite route. Templates are whole Next.js sites. --- # Architecture The house style for building Koala UI - a pragmatic distillation of modern React design-system patterns, taking what scales and dropping what's clever-but-fragile. ## The stack - **Styling / variants**: tailwind-variants (tv) with slots - **Class merge**: tailwind-merge, extended for Koala tokens - **Behavior & a11y**: Radix UI primitives - **Polymorphism (asChild)**: Radix Slot - **Cross-part state**: React Context (typed helper) - **Theming**: CSS variables + semantic tokens + class strategy ## What we reject `recursiveCloneChildren`-style prop distribution (matching children by `displayName` to inject props) is elegant in a demo and a liability at scale: it breaks under minification, breaks when a part is wrapped, and clones the subtree each render. We use **React Context** for cross-part state - what Radix, React Aria, Ark, MUI, and Chakra all do internally. ## Full reference The complete writeup lives in `docs/ARCHITECTURE.md` in the repo. --- # Patterns Component decision guide - when to reach for which piece, with live examples of each pattern in context. ## Select vs. Dropdown Menu Both open a list on click - but they model different interactions. **Select carries a value** (part of a form, trigger echoes the selection). **Dropdown Menu triggers actions** (commands run, trigger stays fixed). Select - picking a value Click an option - the trigger updates to echo your choice. Correct for plan, country, sort order, or any form field. Dropdown Menu - triggering actions The trigger always reads "My account". Clicking an item runs a command - nothing is stored in the trigger. Don't - Dropdown for value selection Click any option. The trigger stays "Plan" - users can't tell what's selected, and the value can't be read as a form field. Quick smell test: should the trigger echo whatever was picked? Use **Select**. Should it stay the same label regardless? Use **Dropdown Menu**. ## Dialog vs. Toast **Dialog demands a decision** - it blocks the page until the user acts. **Toast is a passive observer** - it confirms what already happened and dismisses itself. Dialog - requires a response A destructive action needs explicit confirmation. The page blocks until the user decides - cancel or delete. Toast - status feedback The save already happened. Toast confirms it without interrupting the user's flow - it disappears on its own. Never use a Toast to report an error the user must act on - those get missed. Use a Dialog, or inline validation next to the field instead. ## Tooltip - and when to escalate Tooltips are **supplementary labels**, not content containers - they follow the WAI-ARIA tooltip role strictly. Hover the toolbar: the bubble glides between triggers via `TooltipGroup`. Tooltip - labelling icon controls Hover any button. Labels clarify icons without adding visual weight to the layout. - Never put a link, button, or form inside a Tooltip - screen readers won’t reach it and pointer users can’t reliably click it. - If the overlay needs to be clickable, use a **Dropdown Menu** (supports hover open). - If it needs a form or a destructive action, escalate to a **Dialog**. ## Details that make interfaces feel better Sourced from [Details that make interfaces feel better](https://jakub.kr/writing/details-that-make-interfaces-feel-better) by [Jakub Krehel](https://jakub.kr). Worth reading in full. No single detail here is a feature - each one is a fraction of a second, a pixel, a rendering quirk. Stacked, they’re the difference between a UI that was *built* and one that was *designed*. Tabular numbers Watch the left counter: proportional digits shift width as values change (narrow 1 vs wide 9). The right stays locked. font-variant-numeric: tabular-nums makes every digit equal-width. Concentric border radius outer radius − gap = inner radius. With p-2 (8px) and a rounded-2xl (16px) wrapper, the inner surface wants rounded-lg (8px). Mismatching makes corners look accidental. Animate icons on entry Hover each button. Opacity-only pops the icon in abruptly. Adding scale makes the entry feel physical - the icon grows into place rather than materialising out of nowhere. Shadows over solid borders Layered box-shadows with rgba transparency adapt to any background. Solid borders assume a fixed surface colour; shadows cooperate with whatever is underneath. Image outlines A 1px inset outline at 10% opacity defines the edge of images and gradients against any background - especially on light-on-light or dark-on-dark surfaces where the boundary would otherwise dissolve. Text wrap: balance Distributes a heading's text evenly across lines, preventing a short orphaned word on the last line. Pair with text-wrap: pretty on body copy. Font smoothing -webkit-font-smoothing: antialiased switches macOS from subpixel to grayscale rendering - slightly thinner, crisper strokes. Apply once on the root; already set in globals.css. Interruptible animations Hover out of each bar mid-way. The keyframe bar snaps back to zero - it can't be interrupted. The transition bar reverses smoothly from wherever it is. Staggered enter, subtle exit Items cascade in with 75 ms delays - enter takes 350 ms. Exit fires all at once in 180 ms with no stagger. Exits should be faster and quieter than entrances. Optical alignment A trailing arrow shifts the button's visual weight right - equal padding (24/24) looks off-balance. Reducing the right side to 18px restores the feel. Toggle Show Padding to measure. Built on the principles of [make-interfaces-feel-better](https://jakub.kr/skills), the skill by [Jakub Krehel](https://jakub.kr) that we recommend installing alongside Koala. --- # AI and agents Koala's docs are built for coding agents too: every page as markdown, an llms.txt index, the whole docs in one file, and a skill that teaches an agent Koala's rules. How to use each. ## What there is | What | Where | What it is for | | --- | --- | --- | | Markdown pages | Any docs URL plus `.md`, like `/docs/components/button.md` | One page as clean markdown: its prose, tables and every code sample, without the demo markup. | | Copy page | The top right of every docs page | Copies the page as markdown, opens the markdown, or starts a ChatGPT or Claude chat that reads it. | | llms.txt | [/llms.txt](https://www.koalaui.com/llms.txt) | The index: conventions, install and CLI commands, what is free and what is Pro, and every page with its summary. | | llms-full.txt | [/llms-full.txt](https://www.koalaui.com/llms-full.txt) | Every free docs page (guides, foundations, components) in one file. | | The skill | `npx koalaui-cli@latest skill` | The house rules, tokens, themes and CLI in one SKILL.md, plus a reference per component, written into your project. | ## Claude Code Install the skill in your project. Claude Code loads project skills from `.claude/skills/`, and the skill’s description tells it to follow Koala’s rules whenever you ask for UI. Start a new session after installing it. ```bash npx koalaui-cli@latest skill ``` Commit `.claude/skills/koala-ui/` so everyone on the project works from the same rules, and run the command again after you update Koala. For a page the skill does not cover, a foundation or a section family, give Claude its markdown URL: “Read https://www.koalaui.com/docs/foundations/theming.md and add a theme switch to the header.” ## Cursor, Codex and other agents Install the skill the same way, then point the agent at it. Cursor reads project rules from `.cursor/rules/`; this writes one: ```bash mkdir -p .cursor/rules cat > .cursor/rules/koala-ui.mdc <<'EOF' --- description: Building UI with Koala UI globs: ["**/*.tsx"] alwaysApply: false --- Before writing UI, read .claude/skills/koala-ui/SKILL.md and follow it. Component docs are in .claude/skills/koala-ui/references/.md. EOF ``` Agents that read `AGENTS.md`, Codex and Cursor among them, take the same instruction from there; this appends it: ```bash cat >> AGENTS.md <<'EOF' ## UI This project uses Koala UI. Before writing UI, read .claude/skills/koala-ui/SKILL.md and follow it. Component docs are in .claude/skills/koala-ui/references/.md. EOF ``` To keep the skill somewhere else, pass `--dir`: `npx koalaui-cli@latest skill --dir docs/koala-skill`, and point the rule there. ## ChatGPT, Claude and other chats On any docs page, open the Copy page menu at the top right. Copy page puts the page on your clipboard as markdown to paste into a chat; Open in ChatGPT and Open in Claude start a chat that reads the page for you. For a broader question, hand the assistant [llms.txt](https://www.koalaui.com/llms.txt), the index, or [llms-full.txt](https://www.koalaui.com/llms-full.txt), everything at once. ## What an agent can install An agent runs the same CLI you do. Free items need nothing: every component, the lib helpers and the tokens install anywhere with `npx koalaui-cli@latest add `. Sections, page examples and templates are Pro and install with your license once you have run `npx koalaui-cli@latest login ` on the machine, and an agent working there uses that seat. Without a license, `add` stops with a paywall message, and the skill tells the agent to report it rather than rebuild the section. Pro source is never in the markdown. A section family’s page names its Pro variants and stops there; no `.md` page, llms file or skill reference carries the code of a Pro section, page or template. ## FAQ ### Is the markdown the same as the page? It is rendered from the same page, at build time: the prose, tables and code samples are the page's own. Interactive demos are left out, and every demo's code sample stays. ### How do I keep the skill current? Run the skill command again after you update Koala. It replaces the skill's files and removes references a newer version no longer has; nothing else in your project is touched. ### Does the skill need a connection when the agent works? No. It is plain markdown in your repo, so the agent reads it like any other file. Only the .md pages and llms files live on the site. --- # Migrating from shadcn/ui Most of what you know from shadcn/ui carries over: Radix underneath, Tailwind, the source in your repo. Here is every shadcn component's Koala counterpart, and every name that changes. ## At a glance Where the two meet and where they part. The sections below go through each difference. | | shadcn/ui | Koala UI | | --- | --- | --- | | **Foundations** | | | | Source in your repo | Yes | Yes | | Radix primitives | Yes | Yes | | Tailwind CSS v4 | Yes | Yes | | Variant recipes | `cva` | `tv`, with slots | | Icons | Lucide | Phosphor | | **Theming** | | | | Themes
Light, cream, dark and moonlight, where shadcn/ui ships light and dark. | 2 | 4 | | Accent colors
Switched with a class, no rebuild. | 1 | 8 | | Densities
Comfortable and compact, set once for a whole app shell. | 1 | 2 | | **Components** | | | | Data table, date picker, combobox
Components you configure. In shadcn/ui they are recipes you assemble. | No | Yes | | Rich text editor, file upload, AI chat | No | Yes | | Charts, carousel, drawer, calendar
shadcn/ui wraps Recharts, Embla, Vaul and react-day-picker. | Third-party | Built in | | **Beyond components** | | | | Sections
For marketing, app and store pages, in families: hero, feature, pricing, FAQ, checkout and more. | App blocks | 190 | | Page examples | No | 34 | | Templates | No | 7 | | Updates that keep your edits
`update` replaces the files you did not touch and lists the ones you did. | No | Yes | | **License** | | | | Price
Every component is free. Most sections, the page examples and the templates are Pro. | Free | Free and Pro | ## What carries over - The source lives in your repo under `components/ui/`, and the import paths stay the same: `@/components/ui/button`. - Radix primitives for behavior and accessibility, `asChild` through Radix Slot, `className` merged last with `cn`, and a `data-slot` on every part. - Named exports composed as parts: ``. - Tailwind CSS v4, with theming through CSS variables. ## What changes - **`tv` instead of `cva`.** Variant recipes are tailwind-variants, with `slots` for multi-part components, and `VariantProps` comes from `@/lib/tv`. Nothing in Koala uses class-variance-authority. - **More tokens.** The color roles you know (`background`, `foreground`, `primary`, `muted`, `border`) are joined by `brand` (the accent), `body` for reading copy, `canvas`, `link` and status roles with a `-strong` text step. Four themes (`light`, `cream`, `dark`, `moonlight`) and eight accents replace one light and one dark palette. See [Colors](https://www.koalaui.com/docs/foundations/colors.md). - **The brand leads.** `variant="primary"` is the brand color, where shadcn’s default button is near black. `neutral` is the black one. - **Fields have two parts.** `InputRoot` is the frame and `InputField` the input, so prefixes, suffixes and buttons sit inside one border. The same goes for `TextareaRoot` and `TextareaField`. - **Components, not recipes.** Data table, date picker, combobox (`SelectSearch`) and pagination ship as components you configure, not snippets you assemble. - **Fewer dependencies.** Charts, the carousel, the drawer and the calendar are Koala’s own: no Recharts, Embla, Vaul or react-day-picker. Icons are Phosphor, not Lucide. - **Density and control size.** `size` is `sm` 32, `md` 36 and `lg` 40px on every control, and a `DensityProvider` tightens a whole app shell at once. See [Density](https://www.koalaui.com/docs/foundations/density.md). - **Its own CLI.** `koalaui-cli` records installs in `koala.json` instead of `components.json`, and `diff` and `update` bring upstream changes in without overwriting the files you edited. See [Installation](https://www.koalaui.com/docs/installation.md). ## Component map Every shadcn/ui component and where it lands. A Koala name links to its docs page, where the full API and examples live. | shadcn/ui | Koala UI | What changes | | --- | --- | --- | | Accordion | [Accordion](https://www.koalaui.com/docs/components/accordion.md) | Same parts. Adds a `variant` axis for flush, contained and grid layouts. | | Alert | [Alert](https://www.koalaui.com/docs/components/alert.md) | The glyph goes in `AlertIcon` and the text in `AlertContent`; `AlertActions` holds buttons. Variants: `default`, `success`, `warning`, `info`, `destructive`. | | Alert Dialog | [Dialog](https://www.koalaui.com/docs/components/dialog.md) | `AlertDialog`, `AlertDialogContent`, `AlertDialogAction` → `Dialog`, `DialogContent`, `DialogFooter`
There is no separate alert dialog: a `Dialog` whose footer carries the destructive `Button`. | | Aspect Ratio | None yet, see [Aspect Ratio](https://www.koalaui.com/docs/foundations/aspect-ratio.md) | Tailwind's `aspect-*` utilities on the frame, no component. | | Avatar | [Avatar](https://www.koalaui.com/docs/components/avatar.md) | Same three parts, plus `AvatarStatus`, `AvatarBadge`, a `size` scale and a `shape`. | | Badge | [Badge](https://www.koalaui.com/docs/components/badge.md) | See the variant map below. Adds status and category tones, `dot`, `pill` and `size`. | | Breadcrumb | [Breadcrumb](https://www.koalaui.com/docs/components/breadcrumb.md) | Same parts. | | Button | [Button](https://www.koalaui.com/docs/components/button.md) | See the variant and size map below. `iconOnly` replaces `size="icon"`; `loading` is built in. | | Button Group | [Button Group](https://www.koalaui.com/docs/components/button-group.md) | `Button` inside the group → `ButtonGroupItem`
The group sets `variant` and `size` for its items. | | Calendar | [Date Picker](https://www.koalaui.com/docs/components/date-picker.md) | `Calendar` → `Calendar`
Koala's own calendar: no react-day-picker, no date library. | | Card | [Card](https://www.koalaui.com/docs/components/card.md) | Same parts, plus `CardMedia` for an inset picture. | | Carousel | [Carousel](https://www.koalaui.com/docs/components/carousel.md) | `CarouselItem` → `CarouselSlide`
No Embla dependency. Adds `CarouselIndicators`. | | Chart | [Chart](https://www.koalaui.com/docs/components/chart.md) | `ChartContainer` → `Chart`
`ChartTooltip` + `ChartTooltipContent` → `ChartTooltip`
`ChartLegend` + `ChartLegendContent` → `ChartLegend`
Recharts `Line`, `Area`, `Bar` → `ChartLine`, `ChartArea`, `ChartBar`
Its own SVG charts, no Recharts: `Chart` takes `data`, `index` and `config`, and the series are parts. | | Checkbox | [Checkbox](https://www.koalaui.com/docs/components/checkbox.md) | Same. | | Collapsible | [Collapsible](https://www.koalaui.com/docs/components/collapsible.md) | Same parts, plus `CollapsibleIcon`, a mark that turns with its trigger. The height tween is built in; `className` on `CollapsibleContent` styles the body. | | Combobox | [Select](https://www.koalaui.com/docs/components/select.md) | `Popover` + `Command` recipe → `SelectSearch`, `SelectSearchTrigger`, `SelectSearchContent`, `SelectSearchItem`
A component, not a recipe. For several values, `MultiSelect`. | | Command | [Command](https://www.koalaui.com/docs/components/command.md) | Same parts, `CommandDialog` included. | | Context Menu | [Context Menu](https://www.koalaui.com/docs/components/context-menu.md) | Same parts. | | Data Table | [Data Table](https://www.koalaui.com/docs/components/data-table.md) | TanStack Table recipe → `DataTable`
A component, not a recipe: `columns` and `data` in, with sorting, filters, selection, pagination, grouping and row reorder. | | Date Picker | [Date Picker](https://www.koalaui.com/docs/components/date-picker.md) | `Popover` + `Calendar` recipe → `DatePicker`, `DateRangePicker`
Components, with presets. | | Dialog | [Dialog](https://www.koalaui.com/docs/components/dialog.md) | Same parts, plus `DialogBody`, `DialogIcon` and `DialogStepper`. The overlay is inside `DialogContent`. | | Drawer | [Drawer](https://www.koalaui.com/docs/components/drawer.md) | No Vaul dependency: a bottom sheet is `DrawerContent side="bottom"`. Adds `DrawerBody` and stacked views. | | Dropdown Menu | [Dropdown Menu](https://www.koalaui.com/docs/components/dropdown-menu.md) | Same parts. Rows can carry a description (`DropdownMenuItemText`); a delete row is `variant="destructive"`. | | Empty | [Empty State](https://www.koalaui.com/docs/components/empty-state.md) | `Empty` → `EmptyState`
`EmptyMedia` → `EmptyStateMedia`
`EmptyTitle` → `EmptyStateTitle`
`EmptyDescription` → `EmptyStateDescription`
`EmptyContent` → `EmptyStateActions` | | Field | [Field](https://www.koalaui.com/docs/components/field.md) | `FieldDescription`, `FieldError` → `FieldHint`
`FieldSet` + `FieldLegend` → `FieldGroup`, `FieldGroupLabel`
An error is `hasError` on `Field`: it turns the hint into the message and sets `aria-invalid`. | | Form | [Form](https://www.koalaui.com/docs/components/form.md) | Not a react-hook-form wrapper: `Form` is a plain `
` that sets the rhythm and the control size, and `Field` wires the label and hint. Bring your own validation. | | Hover Card | [Hover Card](https://www.koalaui.com/docs/components/hover-card.md) | Same parts, plus `HoverCardTitle` and `HoverCardDescription`. The card is Popover's surface, with `showArrow` and `density`. | | Input | [Input](https://www.koalaui.com/docs/components/input.md) | `Input` → `InputRoot`, `InputField`
Two parts: `InputRoot` is the frame, `InputField` the ``. Prefixes, suffixes, `PasswordInput`, `NumberInput` and `PhoneInput` included. | | Input Group | [Input Group](https://www.koalaui.com/docs/components/input-group.md) | `InputGroup` joins ordinary Koala controls (an input, a select, a button) into one frame; `InputGroupAddon` holds text or icons. | | Input OTP | [OTP Input](https://www.koalaui.com/docs/components/otp-input.md) | `InputOTP`, `InputOTPGroup`, `InputOTPSlot` → `OTPInput`
One component; the slots come from `length`. | | Item | [List](https://www.koalaui.com/docs/components/list.md) | `ItemGroup` → `List`
`Item` → `ListItem`
`ItemMedia` → `ListItemMedia`
`ItemContent` → `ListItemContent`
`ItemTitle` → `ListItemTitle`
`ItemDescription` → `ListItemDescription`
`ItemActions` → `ListItemMeta` | | Kbd | [Kbd](https://www.koalaui.com/docs/components/kbd.md) | Same. | | Label | [Label](https://www.koalaui.com/docs/components/label.md) | Same, plus `Hint` for the line under a control. | | Menubar | [Menubar](https://www.koalaui.com/docs/components/menubar.md) | Same parts. The menus are Dropdown Menu's panels: a delete row is `variant="destructive"`, and a checkbox row can be `variant="switch"`. | | Native Select | [Native Select](https://www.koalaui.com/docs/components/native-select.md) | Same parts. `size` (sm, md, lg) matches the Select trigger, and inside a `Field` it wires its own id, hint and error state. | | Navigation Menu | [Navbar](https://www.koalaui.com/docs/components/navbar.md) | `Navbar` is the whole site bar: brand, links with dropdowns, actions and the mobile menu. | | Pagination | [Pagination](https://www.koalaui.com/docs/components/pagination.md) | A ready component (`page`, `pageCount`, `onPageChange`); `PaginationRoot` and its parts for a custom layout. | | Popover | [Popover](https://www.koalaui.com/docs/components/popover.md) | Same parts, plus `PopoverTitle`, `PopoverDescription` and `PopoverCloseButton`. | | Progress | [Progress](https://www.koalaui.com/docs/components/progress.md) | Same, plus `ProgressLabel` and `ProgressValue`. | | Radio Group | [Radio Group](https://www.koalaui.com/docs/components/radio-group.md) | Same parts, plus `RadioCard` for option cards. | | Resizable | [Resizable](https://www.koalaui.com/docs/components/resizable.md) | Same parts. | | Scroll Area | [Scroll Area](https://www.koalaui.com/docs/components/scroll-area.md) | `ScrollBar` is not a part: `orientation` (vertical, horizontal, both) draws the bars. Adds `fade` for the edges and `viewportRef`. | | Select | [Select](https://www.koalaui.com/docs/components/select.md) | Same parts. | | Separator | [Divider](https://www.koalaui.com/docs/components/divider.md) | `Separator` → `Divider`
Adds a label and dashed, dotted and gradient strokes. | | Sheet | [Drawer](https://www.koalaui.com/docs/components/drawer.md) | `Sheet` → `Drawer`
`SheetTrigger` → `DrawerTrigger`
`SheetContent` → `DrawerContent`
`SheetHeader` → `DrawerHeader`
`SheetTitle` → `DrawerTitle`
`SheetDescription` → `DrawerDescription`
`SheetFooter` → `DrawerFooter`
`SheetClose` → `DrawerClose`
`side` goes on `DrawerContent`. | | Sidebar | [Sidebar](https://www.koalaui.com/docs/components/sidebar.md) | `SidebarMenuItem` + `SidebarMenuButton` → `SidebarItem`
The shell (`SidebarProvider`, `SidebarInset`, `SidebarTrigger`) is the Layout component: `Layout`, `LayoutSidebar`, `LayoutContent`, `LayoutSidebarTrigger`. | | Skeleton | [Skeleton](https://www.koalaui.com/docs/components/skeleton.md) | Same. | | Slider | [Slider](https://www.koalaui.com/docs/components/slider.md) | Same, plus `SliderInput`: a track beside a value box you can scrub. | | Sonner (and Toast) | [Toast](https://www.koalaui.com/docs/components/toast.md) | `toast` and `` from sonner → `toast`, `Toaster`
The same call shape: `toast()`, `toast.success()`, `toast.error()`, `toast.promise()`. Mount `` once. | | Spinner | [Spinner](https://www.koalaui.com/docs/components/spinner.md) | Same. | | Switch | [Switch](https://www.koalaui.com/docs/components/switch.md) | Same. | | Table | [Data Table](https://www.koalaui.com/docs/components/data-table.md) | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell` → `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`
The same parts, exported from `@/components/ui/data-table`. | | Tabs | [Tabs](https://www.koalaui.com/docs/components/tabs.md) | Same parts. Adds a `variant` (`pill`, `folder`, `line`, `chip`) and `TabsPanels`. | | Textarea | [Textarea](https://www.koalaui.com/docs/components/textarea.md) | `Textarea` → `TextareaRoot`, `TextareaField`
Two parts like Input, plus `TextareaCount` for a live character count. | | Toggle | [Toggle](https://www.koalaui.com/docs/components/toggle.md) | Variants `outline` (the default) and `ghost`; `size` is `sm` or `md`, and `iconOnly` replaces the square size. Adds `ToggleIcon` for a pressed glyph. | | Toggle Group | [Toggle Group](https://www.koalaui.com/docs/components/toggle-group.md) | Same parts. | | Tooltip | [Tooltip](https://www.koalaui.com/docs/components/tooltip.md) | `Tooltip` + `TooltipTrigger` + `TooltipContent` → `Tooltip`
`TooltipProvider` → `TooltipGroup`
One element: `` wraps the trigger, `placement` sets the side. | | Typography | [Prose](https://www.koalaui.com/docs/components/prose.md) | `Prose` styles rendered long-form HTML (a blog post, docs) in one wrapper. | ## Variants and sizes The values that are named differently. Anything not listed keeps its shadcn name. ### Button `variant` | shadcn/ui | Koala UI | Notes | | --- | --- | --- | | `default` | `primary` | The brand color. For shadcn's black button, `neutral`. | | `secondary` | `secondary` | | | `outline` | `outline` | | | `ghost` | `ghost` | | | `destructive` | `destructive` | A soft red tint; `destructiveGhost` for a quiet delete. | | `link` | `link` | | ### Button `size` | shadcn/ui | Koala UI | Notes | | --- | --- | --- | | `sm` | `sm` | 32px | | `default` | `md` | 36px, the default | | `lg` | `lg` | 40px | | `icon` | `iconOnly` | A boolean, combined with any size | | None | `xl` | 44px, for marketing calls to action | ### Badge `variant` | shadcn/ui | Koala UI | Notes | | --- | --- | --- | | `default` | `primary` | Koala's `default` is a muted gray chip. | | `secondary` | `secondary` | | | `outline` | `outline` | | | `destructive` | `destructive` | A soft tint, like the other status tones. | ### Alert `variant` | shadcn/ui | Koala UI | Notes | | --- | --- | --- | | `default` | `default` | | | `destructive` | `destructive` | | | None | `success`, `warning`, `info` | | ## Moving a project over 1. **Clear shadcn’s colors.** Delete the `:root` and `.dark` blocks shadcn added to your global stylesheet (`app/globals.css`, or `src/index.css` in Vite), and its `@custom-variant dark` line: they redefine `--background`, the other roles and `--radius`, and would override Koala’s themes. `init` warns you about each one still there. 2. **Run init.** It writes the tokens to `koala.css` (`app/` in Next.js, `src/styles/` in Vite), the helpers to `lib/` and the theme provider. shadcn’s `lib/utils.ts` is replaced: Koala’s `cn` has the same signature, with tailwind-merge taught Koala’s tokens. If you edited yours, `init` keeps it and tells you what to merge. 3. **Swap one component at a time.** Delete shadcn’s file and add Koala’s. The import path does not change, so only the parts and props in the map above do. `add` refuses while a flat `components/ui/.tsx` is still there, for the component you asked for and for every Koala component it pulls in: the import would resolve to that file instead of Koala’s folder. 4. **Let the type checker find the rest.** Koala’s props are typed, so a leftover shadcn prop or value fails to compile. 5. **Clean up** once nothing imports them: `components.json`, `class-variance-authority`, `lucide-react` and the packages Koala replaced. ```bash # 1. Tokens, the lib helpers and the theme provider (after removing shadcn's color blocks) npx koalaui-cli@latest init # 2. One component at a time: delete shadcn's file, add Koala's rm components/ui/button.tsx npx koalaui-cli@latest add button ``` ## Why Koala - **More of the product, already built.** 123 components, free, including the ones you would otherwise assemble: a data table, date and range pickers, charts, a command palette, a rich text editor, file upload, and AI chat and prompt input. - **Sections, pages and templates.** 190 sections for marketing, app and store pages, in families (hero, feature, pricing, FAQ, checkout and more), 34 page examples and 7 templates, all of them Koala UI Pro. - **Theming with range.** Four themes, eight accents and a radius knob from one layer of tokens: a re-skin is a class, not a fork. - **Two densities.** The same components set a spacious marketing page and a dense dashboard. - **Finish.** Every component goes through the same polish pass: 40px hit areas, concentric radii, tabular numbers, reduced-motion fallbacks and AA contrast in every theme. - **Updates that keep your edits.** `diff` and `update` compare against what was installed, replace the files you did not touch and list the ones you did. ## FAQ ### Can shadcn/ui and Koala share a project while I migrate? Yes, one component at a time. Both live in components/ui with the same import paths, so each component is either shadcn's file or Koala's folder: delete the file before you add the folder (the CLI refuses otherwise, and names the files in the way). The core color roles share their names, so the shadcn components you have not moved yet mostly pick up Koala's colors. ### Can I install Koala with the shadcn CLI? No. Koala installs with its own CLI, koalaui-cli, which records what you installed in koala.json so that later updates keep your edits. ### Do I need to change my Tailwind or React setup? Koala needs Tailwind CSS v4 and React 19. A shadcn project still on Tailwind v3 upgrades Tailwind first; the rest carries over. ### What happens to my Lucide icons? They keep working. Koala's components draw Phosphor icons, and the two can live side by side until you swap yours. --- # Theming Koala is themed entirely through CSS variables. Components read semantic tokens, never a raw color, radius or shadow, so one block of variables restyles the whole system. ## How theming works Every Koala component is built from **semantic color roles** (`bg-background`, `text-foreground`, `bg-primary`,`border-border` …), a single radius knob (`--radius`) and a single accent knob (`--brand`). None of those are hard-coded in a component - they resolve to CSS variables defined once per theme. Switch the variables, and every surface, control and focus ring follows. The roles themselves are documented on the [Colors](https://www.koalaui.com/docs/foundations/colors.md) page. This page is about *changing* them. ```css title="how a component resolves a color" /* A button reads a role, never a literal: */ .button { background: var(--primary); } /* …and the role is defined once, per theme: */ :root { --primary: oklch(0.205 0 0); } /* light */ .cream { --primary: oklch(0.24 0.014 75); } /* cream */ .dark { --primary: oklch(0.922 0 0); } /* dark */ .moonlight { --primary: oklch(0.704 0.14 264); } /* moonlight */ ``` ## The four built-in themes Koala ships **light**, **cream** (warm light), **dark** and **moonlight** (blue-tinted dark). Each is a class on ``; `light` is the bare `:root`, so it needs no class. We drive the class with `next-themes` through a thin `ThemeProvider`. ```tsx title="app/layout.tsx" import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }) { return ( {/* attribute="class" · themes: light, cream, dark, moonlight */} {children} ) } ``` Theme and accent are **orthogonal axes**: any of the nine accents works under any of the four themes, because the themes deliberately never redefine `--brand`. ## Accent color The accent is one variable, `--brand`. Every component reads it through `bg-brand` / `border-brand` / `ring-brand`, so a single value recolors checkboxes, switches, sliders, focus rings and links at once. Nine presets ship as `[data-accent]` blocks - apply one by setting the attribute on ``: ```tsx ``` For a brand color outside the presets, set `--brand` yourself. The focus ring (`--ring-brand`) derives from it automatically. Eight presets are mid-tone and carry white labels; `lime` is light, so its preset follows the theme (a deeper lime on light, the vivid one on dark) and sets `--brand-foreground`, the ink that sits on a brand fill. A light brand color of your own needs the same: set `--brand-foreground` to a dark ink. ```css title="globals.css" :root { --brand: oklch(0.55 0.2 255); /* or any hex / rgb, color-mix converts it */ } ``` ## Corner radius One knob, `--radius`, drives the entire rounding scale. `rounded-lg` is `--radius` itself; `rounded-sm` … `rounded-4xl` are relative multiples of it, so changing the base re-rounds every component concentrically. ```css title="globals.css" :root { --radius: 0.5rem; /* default 0.71rem, set 0 for sharp corners */ } ``` ## Density Density is Koala’s spacing axis - the knob that retunes padding, gaps and control heights without touching color or radius. Koala serves both information-dense application UI and generous marketing surfaces, so `density` lets one subtree tighten or relax with no per-instance props. There are two values: `compact` (the default - 16px padding/gaps, smaller titles, shorter controls) and `comfortable` (the spacious alternative). Wrap a subtree in `DensityProvider` and every nested Koala component follows. The wrapper is `display: contents`, so it never introduces a box into the layout, and it stamps `data-density` on itself as a CSS/debug escape hatch. ```tsx title="app shell" import { DensityProvider } from "@/lib/density" export default function AppShell({ children }) { return ( // Every Koala component inside tightens up. {children} ) } ``` A component resolves its effective density with `useDensity()`. Precedence is **explicit prop > nearest provider > `compact`**, and the result feeds the component’s `tv` recipe. Unlike most Koala contexts, density has a safe default, so it works with no provider at all. ```tsx title="inside a component" import { useDensity, type Density } from "@/lib/density" function Card({ density }: { density?: Density }) { // explicit prop > nearest provider > "compact" const resolved = useDensity(density) return
} ``` Density is **orthogonal** to theme, accent and radius - it changes only spacing, so any density composes with any theme or accent. ## Create your own theme A theme is just a class that redefines the role variables. The fastest path is to copy an existing block (light or dark, whichever is closer) and retune the values - that guarantees you cover every role a component might read. ```css title="app/globals.css" .sunset { /* Surfaces */ --background: oklch(0.98 0.02 70); --foreground: oklch(0.25 0.03 50); --card: oklch(1 0.01 70); --card-foreground: oklch(0.25 0.03 50); --popover: oklch(1 0.01 70); --popover-foreground: oklch(0.25 0.03 50); /* Brand-neutral actions + muted/accent fills */ --primary: oklch(0.45 0.15 30); --primary-foreground: oklch(0.98 0.02 70); --secondary: oklch(0.94 0.03 70); --secondary-foreground: oklch(0.3 0.04 40); --muted: oklch(0.94 0.03 70); --muted-foreground: oklch(0.52 0.03 55); --accent: oklch(0.92 0.04 65); --accent-foreground: oklch(0.3 0.04 40); /* Status, lines + focus */ --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.89 0.03 70); --border-soft: color-mix(in oklch, var(--border) 50%, transparent); --input: oklch(0.89 0.03 70); --ring: oklch(0.7 0.05 60); /* …also copy the success/warning/info, categorical hue, --syntax-* and per-theme --shadow-* roles from a sibling block. */ } ``` Then register the name so `next-themes` can apply it, and it shows up in any theme switcher built from `THEMES`: ```tsx title="components/theme-provider.tsx" export const THEMES = ["light", "cream", "dark", "moonlight", "sunset"] as const ``` **Tip:** a colored container (a tinted hero, a brand panel) should declare `--surface` rather than paint a `--background` block, so nested Inputs, Selects and minimal Tables blend into it instead of cutting their own surface. ## Switching themes at runtime Because the theme is a class, swapping it live is just `setTheme()` from `next-themes`. The header switcher on this site dogfoods exactly this with a `DropdownMenu` radio group. Read `resolvedTheme` rather than `theme`, so a visitor on the system preference still sees a row checked: ```tsx title="theme-switcher.tsx" "use client" import { useTheme } from "next-themes" import { THEMES } from "@/components/theme-provider" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" export function ThemeSwitcher() { const { resolvedTheme, setTheme } = useTheme() return ( {THEMES.map((t) => ( {t} ))} ) } ``` --- # Colors Koala's color system is three layers deep: a literal value, the CSS variable that holds it, and the semantic token a component writes. Components only ever reach for the top layer. ## Three layers It helps to keep three things apart. A **literal value** is a raw, theme-blind color (`#F84416`, `oklch(1 0 0)`). A **CSS variable** (`--background`, `--primary`) holds one literal per theme. A **semantic token** is the utility a component writes (`bg-background`, `text-foreground`). It resolves to the variable, never to a literal. Change a theme by swapping the variables; the literals move, the tokens stay put. ```css title="the chain, bottom to top" /* 1: literal value (raw, theme-blind) */ oklch(0.205 0 0) /* 2: CSS variable, one literal per theme */ :root { --primary: oklch(0.205 0 0); } /* light */ .dark { --primary: oklch(0.922 0 0); } /* dark */ /* 3: semantic token, what a component writes */ @theme inline { --color-primary: var(--primary); } .button { background: var(--color-primary); } /* bg-primary */ ``` The swatches below document the **token and variable** layers, the part you build with. The [literal colors](#literal-colors) at the bottom are the raw escape hatch. For *changing* the variables, see [Theming](https://www.koalaui.com/docs/foundations/theming.md). ## Surfaces Each swatch shows the token a component writes and, below it, the CSS variable it resolves to. ### Backgrounds & surfaces - `bg-background` (`--background`): App background - `bg-canvas` (`--canvas`): App shell frame: black in light themes, the ground in dark ones - `bg-card` (`--card`): Cards, raised panels - `bg-popover` (`--popover`): Popovers, menus - `bg-muted` (`--muted`): Subtle fills - `bg-secondary` (`--secondary`): Secondary fills - `bg-accent` (`--accent`): Hover / active accent ## Content ### Text & foreground - `text-foreground` (`--foreground`): Primary text - `text-body` (`--body`): Reading copy: ledes, quotes, answers - `text-muted-foreground` (`--muted-foreground`): Meta: captions, roles, dates - `text-card-foreground` (`--card-foreground`): Text on cards Inline text links use one role, `text-link`, derived from `--brand` so a link carries the active accent. It stays its own role because it is tuned for *text* where `--brand` is tuned for fills: the raw accent only reaches 3.6:1 as text on white, so the light theme darkens it to clear the 4.5:1 AA floor while the dark themes use it straight. Keep it distinct from `text-info`, which is a status hue and never a link. Links carry a hairline underline at rest and turn both the text and the underline `text-link` on hover. Do not hand-roll that treatment: [use ``](https://www.koalaui.com/docs/components/link.md). ### Links - `text-link` (`--link`): Inline text links (the accent, darkened for text on light) ## Brand & state ### Actions - `bg-primary` (`--primary`): Primary actions - `bg-secondary` (`--secondary`): Secondary actions - `bg-brand` (`--brand`): Accent, set via data-accent - `text-brand-foreground` (`--brand-foreground`): Ink on a brand fill (white, dark on lime) - `bg-destructive` (`--destructive`): Destructive / error ## Status Status roles read at full strength for text and icons; soft badges derive their tint from the same role via opacity (`bg-success/10` + `text-success`). ### Feedback roles - `bg-success` (`--success`): Success / positive - `bg-warning` (`--warning`): Warning / caution - `bg-info` (`--info`): Informational - `bg-destructive` (`--destructive`): Error / danger ## Categorical hues A small, fixed set of distinct hues for labels, tags and category badges. Use these when items need to be told apart, not ranked. ### Label & tag hues - `bg-purple` (`--purple`) - `bg-pink` (`--pink`) - `bg-teal` (`--teal`) - `bg-orange` (`--orange`) ## Lines & focus ### Borders & rings - `border-border` (`--border`): Default borders - `border-input` (`--input`): Form field borders - `ring-ring` (`--ring`): Focus rings ## Literal colors The bottom layer: raw values that do **not** respond to theming. Koala authors these in `oklch` for a wider, more even gamut, but any `color-mix()`-friendly literal works, including plain hex. Reach for a literal only when a color must stay fixed across every theme (a logo, a chart series, a brand mark). For anything that should restyle, use a semantic token above. ### The brand accent (default) - `#F84416`, Hex: the default Koala accent - `oklch(0.648 0.222 34.2)`, The same color, as authored Tailwind v4’s entire oklch palette ships out of the box: 22 families × 11 shades, each a `bg-*` / `text-*` / `border-*` utility (`bg-blue-500`, `text-rose-600`, …). Every chip below is a fixed literal; hover one to read its `family-shade` name and oklch value. | Family | 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | red | `oklch(97.1% 0.013 17.38)` | `oklch(93.6% 0.032 17.717)` | `oklch(88.5% 0.062 18.334)` | `oklch(80.8% 0.114 19.571)` | `oklch(70.4% 0.191 22.216)` | `oklch(63.7% 0.237 25.331)` | `oklch(57.7% 0.245 27.325)` | `oklch(50.5% 0.213 27.518)` | `oklch(44.4% 0.177 26.899)` | `oklch(39.6% 0.141 25.723)` | `oklch(25.8% 0.092 26.042)` | | orange | `oklch(98% 0.016 73.684)` | `oklch(95.4% 0.038 75.164)` | `oklch(90.1% 0.076 70.697)` | `oklch(83.7% 0.128 66.29)` | `oklch(75% 0.183 55.934)` | `oklch(70.5% 0.213 47.604)` | `oklch(64.6% 0.222 41.116)` | `oklch(55.3% 0.195 38.402)` | `oklch(47% 0.157 37.304)` | `oklch(40.8% 0.123 38.172)` | `oklch(26.6% 0.079 36.259)` | | amber | `oklch(98.7% 0.022 95.277)` | `oklch(96.2% 0.059 95.617)` | `oklch(92.4% 0.12 95.746)` | `oklch(87.9% 0.169 91.605)` | `oklch(82.8% 0.189 84.429)` | `oklch(76.9% 0.188 70.08)` | `oklch(66.6% 0.179 58.318)` | `oklch(55.5% 0.163 48.998)` | `oklch(47.3% 0.137 46.201)` | `oklch(41.4% 0.112 45.904)` | `oklch(27.9% 0.077 45.635)` | | yellow | `oklch(98.7% 0.026 102.212)` | `oklch(97.3% 0.071 103.193)` | `oklch(94.5% 0.129 101.54)` | `oklch(90.5% 0.182 98.111)` | `oklch(85.2% 0.199 91.936)` | `oklch(79.5% 0.184 86.047)` | `oklch(68.1% 0.162 75.834)` | `oklch(55.4% 0.135 66.442)` | `oklch(47.6% 0.114 61.907)` | `oklch(42.1% 0.095 57.708)` | `oklch(28.6% 0.066 53.813)` | | lime | `oklch(98.6% 0.031 120.757)` | `oklch(96.7% 0.067 122.328)` | `oklch(93.8% 0.127 124.321)` | `oklch(89.7% 0.196 126.665)` | `oklch(84.1% 0.238 128.85)` | `oklch(76.8% 0.233 130.85)` | `oklch(64.8% 0.2 131.684)` | `oklch(53.2% 0.157 131.589)` | `oklch(45.3% 0.124 130.933)` | `oklch(40.5% 0.101 131.063)` | `oklch(27.4% 0.072 132.109)` | | green | `oklch(98.2% 0.018 155.826)` | `oklch(96.2% 0.044 156.743)` | `oklch(92.5% 0.084 155.995)` | `oklch(87.1% 0.15 154.449)` | `oklch(79.2% 0.209 151.711)` | `oklch(72.3% 0.219 149.579)` | `oklch(62.7% 0.194 149.214)` | `oklch(52.7% 0.154 150.069)` | `oklch(44.8% 0.119 151.328)` | `oklch(39.3% 0.095 152.535)` | `oklch(26.6% 0.065 152.934)` | | emerald | `oklch(97.9% 0.021 166.113)` | `oklch(95% 0.052 163.051)` | `oklch(90.5% 0.093 164.15)` | `oklch(84.5% 0.143 164.978)` | `oklch(76.5% 0.177 163.223)` | `oklch(69.6% 0.17 162.48)` | `oklch(59.6% 0.145 163.225)` | `oklch(50.8% 0.118 165.612)` | `oklch(43.2% 0.095 166.913)` | `oklch(37.8% 0.077 168.94)` | `oklch(26.2% 0.051 172.552)` | | teal | `oklch(98.4% 0.014 180.72)` | `oklch(95.3% 0.051 180.801)` | `oklch(91% 0.096 180.426)` | `oklch(85.5% 0.138 181.071)` | `oklch(77.7% 0.152 181.912)` | `oklch(70.4% 0.14 182.503)` | `oklch(60% 0.118 184.704)` | `oklch(51.1% 0.096 186.391)` | `oklch(43.7% 0.078 188.216)` | `oklch(38.6% 0.063 188.416)` | `oklch(27.7% 0.046 192.524)` | | cyan | `oklch(98.4% 0.019 200.873)` | `oklch(95.6% 0.045 203.388)` | `oklch(91.7% 0.08 205.041)` | `oklch(86.5% 0.127 207.078)` | `oklch(78.9% 0.154 211.53)` | `oklch(71.5% 0.143 215.221)` | `oklch(60.9% 0.126 221.723)` | `oklch(52% 0.105 223.128)` | `oklch(45% 0.085 224.283)` | `oklch(39.8% 0.07 227.392)` | `oklch(30.2% 0.056 229.695)` | | sky | `oklch(97.7% 0.013 236.62)` | `oklch(95.1% 0.026 236.824)` | `oklch(90.1% 0.058 230.902)` | `oklch(82.8% 0.111 230.318)` | `oklch(74.6% 0.16 232.661)` | `oklch(68.5% 0.169 237.323)` | `oklch(58.8% 0.158 241.966)` | `oklch(50% 0.134 242.749)` | `oklch(44.3% 0.11 240.79)` | `oklch(39.1% 0.09 240.876)` | `oklch(29.3% 0.066 243.157)` | | blue | `oklch(97% 0.014 254.604)` | `oklch(93.2% 0.032 255.585)` | `oklch(88.2% 0.059 254.128)` | `oklch(80.9% 0.105 251.813)` | `oklch(70.7% 0.165 254.624)` | `oklch(62.3% 0.214 259.815)` | `oklch(54.6% 0.245 262.881)` | `oklch(48.8% 0.243 264.376)` | `oklch(42.4% 0.199 265.638)` | `oklch(37.9% 0.146 265.522)` | `oklch(28.2% 0.091 267.935)` | | indigo | `oklch(96.2% 0.018 272.314)` | `oklch(93% 0.034 272.788)` | `oklch(87% 0.065 274.039)` | `oklch(78.5% 0.115 274.713)` | `oklch(67.3% 0.182 276.935)` | `oklch(58.5% 0.233 277.117)` | `oklch(51.1% 0.262 276.966)` | `oklch(45.7% 0.24 277.023)` | `oklch(39.8% 0.195 277.366)` | `oklch(35.9% 0.144 278.697)` | `oklch(25.7% 0.09 281.288)` | | violet | `oklch(96.9% 0.016 293.756)` | `oklch(94.3% 0.029 294.588)` | `oklch(89.4% 0.057 293.283)` | `oklch(81.1% 0.111 293.571)` | `oklch(70.2% 0.183 293.541)` | `oklch(60.6% 0.25 292.717)` | `oklch(54.1% 0.281 293.009)` | `oklch(49.1% 0.27 292.581)` | `oklch(43.2% 0.232 292.759)` | `oklch(38% 0.189 293.745)` | `oklch(28.3% 0.141 291.089)` | | purple | `oklch(97.7% 0.014 308.299)` | `oklch(94.6% 0.033 307.174)` | `oklch(90.2% 0.063 306.703)` | `oklch(82.7% 0.119 306.383)` | `oklch(71.4% 0.203 305.504)` | `oklch(62.7% 0.265 303.9)` | `oklch(55.8% 0.288 302.321)` | `oklch(49.6% 0.265 301.924)` | `oklch(43.8% 0.218 303.724)` | `oklch(38.1% 0.176 304.987)` | `oklch(29.1% 0.149 302.717)` | | fuchsia | `oklch(97.7% 0.017 320.058)` | `oklch(95.2% 0.037 318.852)` | `oklch(90.3% 0.076 319.62)` | `oklch(83.3% 0.145 321.434)` | `oklch(74% 0.238 322.16)` | `oklch(66.7% 0.295 322.15)` | `oklch(59.1% 0.293 322.896)` | `oklch(51.8% 0.253 323.949)` | `oklch(45.2% 0.211 324.591)` | `oklch(40.1% 0.17 325.612)` | `oklch(29.3% 0.136 325.661)` | | pink | `oklch(97.1% 0.014 343.198)` | `oklch(94.8% 0.028 342.258)` | `oklch(89.9% 0.061 343.231)` | `oklch(82.3% 0.12 346.018)` | `oklch(71.8% 0.202 349.761)` | `oklch(65.6% 0.241 354.308)` | `oklch(59.2% 0.249 0.584)` | `oklch(52.5% 0.223 3.958)` | `oklch(45.9% 0.187 3.815)` | `oklch(40.8% 0.153 2.432)` | `oklch(28.4% 0.109 3.907)` | | rose | `oklch(96.9% 0.015 12.422)` | `oklch(94.1% 0.03 12.58)` | `oklch(89.2% 0.058 10.001)` | `oklch(81% 0.117 11.638)` | `oklch(71.2% 0.194 13.428)` | `oklch(64.5% 0.246 16.439)` | `oklch(58.6% 0.253 17.585)` | `oklch(51.4% 0.222 16.935)` | `oklch(45.5% 0.188 13.697)` | `oklch(41% 0.159 10.272)` | `oklch(27.1% 0.105 12.094)` | | slate | `oklch(98.4% 0.003 247.858)` | `oklch(96.8% 0.007 247.896)` | `oklch(92.9% 0.013 255.508)` | `oklch(86.9% 0.022 252.894)` | `oklch(70.4% 0.04 256.788)` | `oklch(55.4% 0.046 257.417)` | `oklch(44.6% 0.043 257.281)` | `oklch(37.2% 0.044 257.287)` | `oklch(27.9% 0.041 260.031)` | `oklch(20.8% 0.042 265.755)` | `oklch(12.9% 0.042 264.695)` | | gray | `oklch(98.5% 0.002 247.839)` | `oklch(96.7% 0.003 264.542)` | `oklch(92.8% 0.006 264.531)` | `oklch(87.2% 0.01 258.338)` | `oklch(70.7% 0.022 261.325)` | `oklch(55.1% 0.027 264.364)` | `oklch(44.6% 0.03 256.802)` | `oklch(37.3% 0.034 259.733)` | `oklch(27.8% 0.033 256.848)` | `oklch(21% 0.034 264.665)` | `oklch(13% 0.028 261.692)` | | zinc | `oklch(98.5% 0 0)` | `oklch(96.7% 0.001 286.375)` | `oklch(92% 0.004 286.32)` | `oklch(87.1% 0.006 286.286)` | `oklch(70.5% 0.015 286.067)` | `oklch(55.2% 0.016 285.938)` | `oklch(44.2% 0.017 285.786)` | `oklch(37% 0.013 285.805)` | `oklch(27.4% 0.006 286.033)` | `oklch(21% 0.006 285.885)` | `oklch(14.1% 0.005 285.823)` | | neutral | `oklch(98.5% 0 0)` | `oklch(97% 0 0)` | `oklch(92.2% 0 0)` | `oklch(87% 0 0)` | `oklch(70.8% 0 0)` | `oklch(55.6% 0 0)` | `oklch(43.9% 0 0)` | `oklch(37.1% 0 0)` | `oklch(26.9% 0 0)` | `oklch(20.5% 0 0)` | `oklch(14.5% 0 0)` | | stone | `oklch(98.5% 0.001 106.423)` | `oklch(97% 0.001 106.424)` | `oklch(92.3% 0.003 48.717)` | `oklch(86.9% 0.005 56.366)` | `oklch(70.9% 0.01 56.259)` | `oklch(55.3% 0.013 58.071)` | `oklch(44.4% 0.011 73.639)` | `oklch(37.4% 0.01 67.558)` | `oklch(26.8% 0.007 34.298)` | `oklch(21.6% 0.006 56.043)` | `oklch(14.7% 0.004 49.25)` | **Rule of thumb:** if it should change when the theme changes, it is a **token**: reach for a role above. If it must stay exactly that color forever, it is a **literal**. --- # Typography Two typefaces: Inter carries UI and body copy, DM Sans the headings, with the system monospace as the code fallback. The scale, weights, tracking and leading come straight from Tailwind v4. ## Font families Two bundled typefaces, both variable fonts loaded via `next/font`. `font-sans` (Inter) is the workhorse for UI, body and the smallest headings (`h5`–`h6`); `font-heading` (DM Sans, with an optical-size axis) carries the headings `h1`–`h4`, so the display voice reaches the content-level titles too. `font-mono` falls back to the platform UI monospace - nothing to download. A heading that acts as a 14px UI label rather than a title opts back into Inter with a plain `font-sans`, which beats the bare element rule. font-sans The quick brown fox font-heading The quick brown fox font-mono const koala = true ## Type scale Sizes are absolute (`rem`). Each ships a paired line-height that snaps the rendered leading onto the 4px / 0.25rem grid - the lead column. - `text-xs`: 0.75rem · 12px (lh 1.333 → 1rem / 16px) - `text-sm`: 0.875rem · 14px (lh 1.429 → 1.25rem / 20px) - `text-base`: 1rem · 16px (lh 1.5 → 1.5rem / 24px) - `text-lg`: 1.125rem · 18px (lh 1.556 → 1.75rem / 28px) - `text-xl`: 1.25rem · 20px (lh 1.4 → 1.75rem / 28px) - `text-2xl`: 1.5rem · 24px (lh 1.333 → 2rem / 32px) - `text-3xl`: 1.875rem · 30px (lh 1.2 → 2.25rem / 36px) - `text-4xl`: 2.25rem · 36px (lh 1.111 → 2.5rem / 40px) - `text-5xl`: 3rem · 48px (lh 1 → 3rem / 48px) These are raw sizes, not heading roles. A bare `h1`–`h4` carries the DM Sans face (with the negative tracking on `h1`–`h2` only) while `h5`–`h6` stay in Inter (see Font families), but none carry an intrinsic size, so each context steps off this scale on its own. For the document scale the [Rich Text Editor](https://www.koalaui.com/docs/components/rich-text-editor.md#prose-scale) maps onto `H1`–`H4` and body copy, see its Prose scale. ## Weights Four stops off the variable weight axis, shared by both faces. We skip 100–300 (too fragile at text sizes) and everything above 700 (heavier than the brand needs). - `font-normal`: 400 (body) - `font-medium`: 500 (UI labels) - `font-semibold`: 600 (headings) - `font-bold`: 700 (emphasis) ## Tracking `letter-spacing` in `em`, so it scales with size. The px column is resolved at this 18px sample. Rule of thumb: tighten large display text (`tracking-tight`), leave body at normal, and only open up small all-caps labels. - `tracking-tighter`: -0.05em (−0.90px @ 18px) - `tracking-tight`: -0.025em (−0.45px @ 18px) - `tracking-normal`: 0em (0px @ 18px) - `tracking-wide`: 0.025em (+0.45px @ 18px) - `tracking-wider`: 0.05em (+0.90px @ 18px) - `tracking-widest`: 0.1em (+1.80px @ 18px) ## Leading Unitless `line-height`, multiplied by the font-size. The px column is resolved at this 16px sample. Most components inherit the type scale’s paired leading; reach for these only to override it. - `leading-tight`: 1.25 (20px @ 16px) - `leading-snug`: 1.375 (22px @ 16px) - `leading-normal`: 1.5 (24px @ 16px) - `leading-relaxed`: 1.625 (26px @ 16px) - `leading-loose`: 2 (32px @ 16px) --- # Spacing & Layout Spacing and container widths are Tailwind v4 defaults, used as-is, with one addition (--container-8xl, 1440px) kept as an opt-in escape above the canonical 1280px section cap. ## Spacing scale Every `p-*`, `m-*`, `gap-*` and `size-*` step is `0.25rem × n`. - `1`: 0.25rem - `2`: 0.5rem - `3`: 0.75rem - `4`: 1rem - `6`: 1.5rem - `8`: 2rem - `12`: 3rem - `16`: 4rem - `24`: 6rem ## Containers - `max-w-sm`: 24rem - `max-w-md`: 28rem - `max-w-lg`: 32rem - `max-w-2xl`: 42rem - `max-w-3xl`: 48rem - `max-w-5xl`: 64rem - `max-w-7xl`: 80rem --- # Breakpoints The five responsive thresholds Koala builds on: Tailwind v4 defaults, used verbatim. Styles are mobile-first: an unprefixed utility applies everywhere, a prefixed one applies from that width up. ## The scale Each breakpoint is a `min-width` media query. The bar shows where each one kicks in, relative to the `2xl` ceiling. - `sm`: 40rem (≥ 640px) - `md`: 48rem (≥ 768px) - `lg`: 64rem (≥ 1024px) - `xl`: 80rem (≥ 1280px) - `2xl`: 96rem (≥ 1536px) ## Mobile-first Write the small-screen layout with unprefixed utilities, then layer overrides at each breakpoint and up. Don't use `sm:` to target only mobile: it means *640px and wider*. ```tsx title="responsive.tsx" // Stacked on phones, side-by-side from 640px (sm), wider gap from 1024px (lg).
…
``` ## Targeting a single range For styles that should apply *below* a breakpoint, use the `max-*` variants. Combine a min and a max prefix to scope a utility to a single band. ```tsx title="ranges.tsx"
Hidden below 640px
Tinted only between 768px and 1024px
``` ## Previewing breakpoints Media queries respond to the *viewport*, so you can't see a layout reflow just by resizing a column. The [Marketing Sections](https://www.koalaui.com/marketing/sections/hero.md) preview renders each section inside an iframe, so its own breakpoints respond to the frame width, so switch between Mobile (375px), Tablet (768px) and Desktop, or drag the handle to any width in between. --- # Density Density is the cross-cutting knob that retunes padding, gaps and control heights without touching color or radius. comfortable is the marketing-first default; compact is the application-UI preset. ## Two densities The same Card, rendered `comfortable` (the default) and `compact`. Only spacing and title scale change - corners stay the brand 16px radius. ```tsx … … ``` ## On a real block Density isn’t a one-component knob. Here it drives a whole [sign-in block](https://www.koalaui.com/docs/components/auth-form.md): toggle it and the card retunes all at once - `comfortable` opens the padding, gaps and title up for a roomier marketing feel, `compact` tightens everything for dense app UI. ## Application-UI spec The compact preset, modelled on a dense dashboard: 16px padding and gaps, 1rem card titles, 14px muted descriptions, and controls that aren’t too tall. | | comfortable | compact | | --- | --- | --- | | Card padding / gap | p-6 · gap-6 | p-4 · gap-4 (16px) | | Card title | text-lg | text-base (1rem) | | Card description | text-sm muted | text-sm muted | | Button height (default md) | h-9 (36px) | h-9 (36px) | | Dialog padding / gap | p-6 · gap-4 | p-4 · gap-3 | | Tabs pill | rounded-xl p-1 | rounded-lg p-0.5 | ## Set it once for a subtree Wrap an app shell in `DensityProvider` and every nested Koala component tightens up - no per-instance props. Resolution precedence is **explicit prop > nearest provider > comfortable**, so any component can opt back out. ```tsx title="app-shell.tsx" import { DensityProvider } from "@/lib/density" export function Dashboard({ children }) { // Everything inside renders compact unless a component sets density itself. return {children} } ``` ```tsx … ``` ## Which components honor density Density reaches across the library: controls (**Button**, **Select**, **Tabs**, **Pagination**), surfaces (**Card**, **Dialog**, **Drawer**), data display (**Stat**, **List**, **Description List**, **Data Table**) and the form blocks all retune. **Avatar** and **Badge** opt out by design - they already own explicit pixel `size` scales, so a spacing axis would be redundant. Reading density makes a component a client component. Card, Dialog and Tabs already were; **Button** is now `"use client"` too, so it can participate in provider-driven density. --- # Radius & Shadows The surface foundation. Radius is driven by a single --radius knob, so the whole scale re-rounds from one value. Shadows carry per-theme values so dark surfaces stay legible. ## Radius The scale is relative to `--radius` (`1rem` / 16px), so `rounded-lg` = the knob itself. Quarter-step multipliers keep every value on the 4px grid; px below are resolved at a 16px root. - `rounded-sm`: 8px · 0.5rem · 0.5× - `rounded-md`: 12px · 0.75rem · 0.75× - `rounded-lg`: 16px · 1rem · 1× - `rounded-xl`: 20px · 1.25rem · 1.25× - `rounded-2xl`: 24px · 1.5rem · 1.5× - `rounded-3xl`: 28px · 1.75rem · 1.75× - `rounded-4xl`: 32px · 2rem · 2× ## Shadows / elevation Switch to a dark theme in the header - the shadows shift to heavier, cooler values automatically. - `shadow-xs` - `shadow-sm` - `shadow-md` - `shadow-lg` - `shadow-xl` --- # Aspect Ratio The shapes media is framed in. Koala uses Tailwind's aspect-* utilities as-is: pick the ratio on the frame, let the image crop to fill it, and the layout holds its shape before the pixels arrive. ## Reframing Switch between ratios to see one photo reframed. The frame keeps its ratio at any width, and the image crops around its center. Turn off *Crop to fill* to letterbox it instead. ## Common ratios Eight ratios cover almost every surface. Landscape ones suit heroes, covers and video; portrait ones suit people, posters and phone screens. ## Usage Put the ratio on the frame, not on the image. The frame reserves its space on first paint, so nothing shifts when the photo loads. The image fills it with `object-cover`. Keywords cover the two common cases (`aspect-square`, `aspect-video`), and any other ratio is a fraction: `aspect-4/5`. ```tsx title="cover.tsx"
{alt}
``` ## Cover or contain `object-cover` fills the frame and trims whatever falls outside it, which is right for photography. `object-contain` shows the whole image and leaves bars on the frame's `bg-muted`. Use it for logos, screenshots and diagrams, where a cropped edge loses information. Move the focal point of a crop with `object-top` or `object-[50%_30%]`. ```tsx title="contain.tsx" // A screenshot: never crop it, letterbox it.
{alt}
``` --- # Motion Three easing curves and three durations, exposed as Tailwind utilities and mirrored in lib/motion.ts for JS animation. Never inline a raw cubic-bezier or ms value. ## Easing Same duration, different curves - press Play to compare. ## Duration Same curve (`ease-out`), different durations. ## Spring: overshoot for confirmation A fourth curve, `ease-spring`, overshoots past its end value before settling back. Reserve it for moments that should feel rewarding - a confirmation, a checkmark, a control that pops into place - never for routine UI movement, where the bounce reads as nervous. Press Play to compare the overshoot against a flat `ease-out`. ```tsx title="confirm.tsx" // Spring overshoots past the end, then settles - a "pop" on confirmation.
// JS-driven animation import { easing, duration } from "@/lib/motion" element.animate(keyframes, { duration: duration.base, // 300 easing: easing.spring, // cubic-bezier(0.34, 1.56, 0.64, 1) }) ``` ## Sine: for values that oscillate `ease-sine` is symmetric and leaves and arrives at zero velocity. Reach for it when a value swings back and forth through a multi-stop `@keyframes` rather than travelling from A to B once - an error shake, a pendulum, a breathing loop. The reason is easy to miss: a timing function applies to *every segment between stops*, so `ease-out` makes each swing shoot to its extreme and stall there, and the motion reads as a buzz instead of a wobble. ```tsx title="globals.css" /* Keyframe only the extremes; ease-sine draws the arc between them. */ --animate-otp-shake: otp-shake 650ms var(--ease-sine); @keyframes otp-shake { 0%, 100% { transform: translate3d(0, 0, 0); } 17% { transform: translate3d(calc(var(--otp-shake-distance) * -1), 0, 0); } 50% { transform: translate3d(calc(var(--otp-shake-distance) * 0.55), 0, 0); } 83% { transform: translate3d(calc(var(--otp-shake-distance) * -0.25), 0, 0); } } ``` ## Usage Compose the utilities directly in components, or import the constants from `lib/motion.ts` when animating from JS. ```tsx title="example.tsx" // CSS / Tailwind - preferred
// JS-driven animation import { easing, duration } from "@/lib/motion" element.animate(keyframes, { duration: duration.base, // 300 easing: easing.out, // cubic-bezier(0.23, 1, 0.32, 1) }) ``` ## Interruptible transitions Use CSS `transition` for any animation driven by interactive state - hover, focus, active, data attributes. CSS transitions can be reversed mid-flight; keyframe animations cannot. Reserve `@keyframes` for staged sequences that run once (spinners, skeleton pulses, entrance choreography). ```tsx title="button.tsx" // ✓ CSS transition - interruptible, reverses cleanly on mouse-out // ✗ Keyframe - fires once, cannot be reversed mid-animation ``` ## Specify properties, never transition-all `transition: all` triggers a style recalc on every CSS property change, including layout-affecting ones like `width` and `padding`. Always name the properties you actually animate. Tailwind’s `transition-transform` covers `transform, translate, scale, rotate` in one utility. ```tsx title="card.tsx" // ✓ Specific - only the GPU-composited properties
// ✗ Transitions layout + paint + composite on every change
``` ## Press and hover feedback Every interactive control confirms a press with a small scale-down - `active:scale-[0.96]`, never below 0.95 - and cards add a subtle hover lift. The feedback is what makes a surface feel tappable. Hover and press the cards below; expose a `static` prop to opt out where the motion would be distracting. One Tailwind v4 trap lives here: `scale-*` and `translate-*` set their own standalone CSS properties, not `transform`. A `transition-[box-shadow,transform]` will silently fail to tween them - they snap. Name `scale` and `translate` in the list, or use the `transition-transform` utility, which already covers all four. ```tsx title="action-card.tsx" // ✓ Press scale + hover lift, with scale/translate named in the transition.
``` `Stagger` animates on mount by default: the right gate for “load” cascades. With stable keys, reordering reuses the DOM and won’t replay; genuinely new content (a filter, a fresh page, a layout switch) mounts and cascades. The DataTable’s cards layout uses it. Tune the gap with `step` (default 70 ms); add `blur` for the 4px → 0 focus pull and `inView` to hold the cascade until it scrolls into view (the same props a [Section Header](https://www.koalaui.com/docs/components/section-header.md) takes, so a header and the grid below it can reveal as one). Direct children must forward `className` and `style`. ## Exit animations: stay subtle Exit animations should be softer than enters. A full-height collapse or a large translate draws too much attention to something leaving. Use a small fixed `translateY` (4–8 px) with a fade - the element slips away rather than crashing out. ```tsx title="toast.tsx" // ✓ Subtle exit - short distance, fades out // Enter: slides up 8px + fades in // Exit: slides down 4px + fades out (half the travel, softer)
// ✗ Full collapse - too dramatic
``` ## Scale from the origin A surface that scales open - a menu, a popover, a tooltip - should grow out of the control that opened it, not its own center. A menu that zooms from its midpoint looks like it materialized in space; one anchored to its trigger looks like it unfolded from the button. Radix exposes the resolved corner as a CSS variable, so pin `transform-origin` to it and let the `zoom-in-95` / `zoom-out-95` enter and exit do the rest. Open both panels and watch where each one grows from. ## Morphing A surface that changes size along with what it shows (a pill opening into a card, a dialog moving to a taller step) should not swap one box for another. It keeps one box, eases it to the new size and crossfades the contents inside. The technique, `useMorphBox`, and its rules have a page of their own: [Morphing](https://www.koalaui.com/docs/foundations/morphing.md). ## Icon animations Never toggle icon visibility with a conditional render - the outgoing icon disappears instantly with no exit animation. Keep both icons in the DOM, position the secondary one absolutely, and cross-fade using `opacity`, `scale`, and `blur`. Enter: scale `0.25 → 1`, opacity `0 → 1`, blur `4px → 0`. ```tsx title="copy-button.tsx" "use client" import { Check, Copy } from "@phosphor-icons/react" import { cn } from "@/lib/utils" const iconClass = (visible: boolean) => cn( "transition-[opacity,transform,filter] duration-fast", // cubic-bezier(0.2,0,0,1) - the snappy curve for icon swaps "[transition-timing-function:cubic-bezier(0.2,0,0,1)]", visible ? "scale-100 opacity-100 blur-0" : "scale-[0.25] opacity-0 blur-[4px]", ) export function CopyButton({ onCopy }: { onCopy: () => void }) { const [copied, setCopied] = React.useState(false) return ( ) } ``` ## Disclosure carets: turn, don’t swap The section above cross-fades two glyphs that mean *different things*. A caret reporting open or closed is the opposite case: it is one mark pointing two ways, so it **rotates**. Swapping a `CaretUp` for a `CaretDown` throws away the thing that carried the meaning, which is the turn itself. Two utilities carry it, and which one you use is part of the convention rather than a judgement call. `caret-turn` (`duration-base`) is for a caret answering a **content reveal**, so the glyph and the height it announces move on one clock. `caret-turn-fast` (`duration-fast`) is for a caret on a **popover trigger**, which opens quicker than a region reveals. Both bundle the reduced-motion stand-down, so it cannot be forgotten; the rotation itself stays at the call site, because it varies (180° for a panel, 90° for a tree branch, 45° for a plus becoming a cross). ```tsx title="disclosure.tsx" // ✓ One glyph, turned by state. The beat and the reduced-motion guard ride along. // ✓ A popover trigger opens faster than a region reveals. // ✗ Two glyphs swapped: the turn, which was the whole signal, is gone. {open ? : } // ✗ Longhand: animates, but never stands down under reduced motion. ``` ## Skip animation on page load Elements that are visible on first render should not animate in - the user never asked for that transition. Gate CSS transitions behind a `data-mounted` attribute set in a `useEffect`. The attribute is absent during SSR and on the first paint, so the transition rule never fires. ```tsx title="panel.tsx" "use client" import * as React from "react" export function Panel({ children }: { children: React.ReactNode }) { const ref = React.useRef(null) // Set the attribute after the first paint - transitions only fire after this. React.useEffect(() => { ref.current?.setAttribute("data-mounted", "") }, []) return (
{children}
) } ``` ## Respect reduced motion Some users set `prefers-reduced-motion` to avoid animation that triggers nausea or distraction. Honor it: a one-shot entrance should drop to an instant appear, and any ambient loop - a spinner, a shimmer, a marquee - should hold still. Tailwind’s `motion-reduce:` and `motion-safe:` variants cover the inline cases; the `Stagger` primitive already gates itself this way. Flip the preference below and Replay to feel the difference a reduced-motion user gets: the same grid, but the cascade is gone. ## will-change `will-change` promotes an element to its own GPU layer before the animation starts, eliminating first-frame stutter. Only use it on properties the GPU can composite: `transform`, `opacity`, and `filter`. Overusing it wastes VRAM and can slow down everything else. Add it only when you observe a visible first-frame glitch. ```tsx title="drawer.tsx" // ✓ Composited property - GPU can handle this without a layout pass
// ✗ will-change: all - promotes the element for *every* property, wastes VRAM
// ✗ Left on permanently - keeps the layer alive even when not animating // Only add will-change while animating; remove it after. ``` --- # Morphing A surface that changes what it shows eases to its new size while its contents crossfade inside, so the eye follows one object instead of watching one leave and another arrive. ## One object, not two A pill turns into a card, a rail into a menu, a dialog moves to a taller step. The easy version swaps one box for another in a single frame: everything around it jumps, and the reader has to find their place again. A morph keeps one box on screen and eases it to the new size while the contents crossfade in place, so the change reads as the same object doing something new. Run the export in both modes. The box growing is also the entrance. Nothing inside it needs to slide or stagger in, because the continuity is already carried by the one surface that never left. ## Measure where it’s going The box never measures itself. It measures an inner *viewport* that is free to be its natural size. The moment the contents change, the viewport is already at the new size while the box is still at the old one, so a `ResizeObserver` on the viewport reads where the box is going rather than where it is. Written back as an inline width and height, that gives CSS two real numbers to tween between, and the clip on the box reveals the new view as it grows around it. Watch the dashed outline land first. `useMorphBox` from `lib/morph.ts` is the whole technique: one hook and one class list. Spread `morphBox` and the returned `style` on the box, put the `ref` and `viewportClassName` on the viewport, and feed `ready` to `data-morph-ready`. ```tsx title="wizard-body.tsx" import { morphBox, useMorphBox } from "@/lib/morph" export function WizardBody({ step }: { step: React.ReactNode }) { // Destructure: reading morph.style off the object trips react-hooks/refs. const { ref, style, ready, viewportClassName } = useMorphBox("height") return ( // The box: eases to whatever the viewport measures, and clips so it reads as one object.
{/* The viewport: its natural size, already at the next step. */}
{step}
) } ``` Two details inside the hook are worth knowing. It reads the layout box (`offsetWidth`, `offsetHeight`), never a rect, so a press-scale on a child, which is a transform, can never feed jitter back into the size. And it takes a callback ref rather than a ref and a mount effect, because the measured node usually arrives later than the component that owns the hook: a dialog’s content, a menu’s popper, a panel behind a flag. ## Pick the axis Tell the hook which dimensions the box owns. The viewport’s sizing follows from it, which is why `viewportClassName` comes back with the rest: a box that owns its width needs a viewport free to be wider than it is, and one that only owns its height needs a viewport that fills it. - `"height"`: w-full (Tabs, dialogs) - `"both"`: w-max (Island, Dock) - `"width"`: w-max (Inline chips) ## The shape rides the size When a box changes shape because it changes size, keep one radius and let the geometry do the work. Any radius past half the height is clamped to exactly half, so `rounded-2xl` is a perfect stadium on a 44px row and a card’s corner once the box grows. Tweening `rounded-pill` into `rounded-2xl` instead looks broken: the pill radius stays past half the height for nearly the whole run, so the box balloons into a capsule and only squares off in the last frames. Both end states are identical; play them and watch the corners in between. When the corner has to change on purpose, derive it from the size the box is going to instead of switching classes. Island writes its measured height to a variable and resolves the corner with one formula: half the height while it is one row, capped at `radius-4xl` once it grows into a card, and on the radius knob throughout. The corner then eases with the box, between two values that are both real. ```tsx title="surface.tsx" // ✓ One radius: clamped to a stadium while short, a card's corner once tall.
// ✓ A corner that must change, derived from the height it is going to (Island).
// ✗ Two classes tweened: a capsule for most of the run, then a snap.
``` ## Contents crossfade in place The box carries the movement, so the contents should not travel. The view on screen blurs in (`fade-in-0`, `zoom-in-96`, `blur-in-4`) while the view leaving is lifted out of flow, pinned to the centre where it was, and blurs out over the same beat. Lifting it out is what lets the viewport measure the incoming view at once; left in flow, it would hold the box at the old size until its exit had played. Run the box and the crossfade on one clock. A box that lands before its contents, or contents still settling in a box that has stopped, read as two animations. Island runs both on `duration-fast`; the Dock, whose panel is larger, on `duration-base`. When the change has a direction, drilling into a layer and back out, say which way with two pixels of slide on the incoming layer only. ```tsx title="view.tsx" // The view on screen: blurs in on the box's clock. "data-[state=enter]:animate-in data-[state=enter]:fade-in-0 data-[state=enter]:zoom-in-96 data-[state=enter]:blur-in-4", // The view leaving: out of flow and centred, so the viewport already measures the next one. "data-[state=exit]:absolute data-[state=exit]:top-1/2 data-[state=exit]:left-1/2 data-[state=exit]:w-max", "data-[state=exit]:-translate-x-1/2 data-[state=exit]:-translate-y-1/2", "data-[state=exit]:animate-out data-[state=exit]:fade-out-0 data-[state=exit]:zoom-out-98", "data-[state=exit]:blur-out-4 data-[state=exit]:fill-mode-forwards", // Keyframes only. A duration with no property list transitions `all`, and the // leaving view would slide into its centring translate instead of taking it. "transition-none duration-base ease-out motion-reduce:animate-none", // Layers with a direction (Dock): in from the right going deeper, from the left coming back. "data-[direction=forward]:slide-in-from-right-2 data-[direction=backward]:slide-in-from-left-2", ``` The leaving view is `inert` and `aria-hidden`, and it usually holds the control that was just pressed (Back, Dismiss). Hand focus to the first control on screen before the browser drops it on the body, and announce the new state in a polite live region if the view that arrives has nothing to focus. ## Nothing morphs on load A surface that is on screen at first paint was not asked to change, so it should not grow in from zero. `morphBox` only transitions under `data-morph-ready`, which the hook sets once a measurement exists: the server paints the natural size, the first measurement lands without a tween, and only the changes after it ease. Arm the crossfade the same way, on the first change of view. When the measured node leaves, the hook drops its size with it, so a dialog that closes and opens again starts from its own content instead of easing down from whatever the last one happened to be. Under reduced motion `morphBox` stands down (`motion-reduce:transition-none`): the box takes the new size at once and the leaving view goes instead of blurring out. The change still happens; it just stops moving. ## Where not to morph **It animates layout.** Width and height re-run layout every frame, for the box and whatever flows around it. That is cheap on a pill, a menu or a dialog body and expensive on a full-page panel. A drawer or a sheet still moves with `translate`. **The clip is the point, and it cuts.** `overflow-hidden` is what makes the morph read as one object, and it also clips focus rings and anything not portalled. Leave the box room for a ring, draw rings inset the way `TabsPanels` does, or pad the viewport and give the space back with a negative margin, the way Island’s swap regions do (`p-1` with `-m-1`). **Content without a ceiling turns it back into a jump.** Past a certain height the ease stops reading as movement and starts reading as lag. Cap the box with a max height and scroll inside it. **Don’t cascade inside it.** Rows that stagger up through a growing box fight the box. The morph is the entrance, so let the rows arrive with it. ## In the library - [Island](https://www.koalaui.com/docs/components/island.md) A floating pill whose views crossfade while it eases to each one’s size, corner included. - [Dock](https://www.koalaui.com/docs/components/dock.md) A rail of icons that opens, in place, into a labelled menu with drill-down layers. - [Tabs](https://www.koalaui.com/docs/components/tabs.md) `TabsPanels` eases the block’s height between panels of different lengths. - [Animated Label](https://www.koalaui.com/docs/components/animated-label.md) The label rolls to its new copy while its box eases between the two widths. - [Button](https://www.koalaui.com/docs/components/button.md) `swapKey` hands the label to Animated Label, so Next to Done morphs instead of jumping. - [Carousel](https://www.koalaui.com/docs/components/carousel.md) The active dot grows into a pill: `rounded-full` rides the width, one radius the whole way. --- # Fade A soft mask that dissolves a box into its surface at its edges. Use the static fade-* utilities for a constant bleed, or the scroll-fade-* siblings to track scroll position. Pure CSS, no JS. ## Static fade The base primitive: a constant `mask-image` gradient that fades one or more edges into the surface, no matter the scroll position. Reach for it whenever the bleed is fixed - a marquee that drifts in from both sides, a masonry that clips at the section edges, or a preview that peeks up from the bottom of its frame. ```tsx title="logos.tsx" // A marquee whose tiles dissolve into the band at both edges, at any scroll position.
{/* Render the set twice so the -50% marquee loops with no seam. */}
{[...logos, ...logos].map((logo, i) => ( ))}
``` - `fade` - `fade-x` - `fade-t` - `fade-b` - `fade-l` - `fade-r` Depth per edge is the `--fade-size` knob (default `min(2.5rem, 12%)`); set it with an arbitrary property like `[--fade-size:6%]`. Inside that band the alpha runs on an eased smoothstep, not a straight line: a linear ramp turns a corner at each end of the band and the eye reads those corners as edges, which is exactly what the fade is there to avoid. It only gets more visible the deeper the band, so a long dissolve like `LoadMore`’s depends on it. The rest of this page covers `scroll-fade`, the same mask made to track scroll progress instead. ## Scroll-aware preview Scroll the list. At the top the leading edge is crisp; once content slides under it, the top fades while the bottom keeps hinting at more below. Change the fade depth to feel the `--scroll-fade-size` knob. ## How it works The fade is a `mask-image` linear gradient whose two inner stops are driven by `animation-timeline: scroll(self)` - a scroll-progress timeline, not a clock. Each edge’s depth is a registered`@property` number from `0` (crisp) to `1` (faded), so it interpolates as you scroll: the top fades in over the first `--scroll-fade-reveal` of travel, and the bottom sharpens over the last. No scroll listeners, no JavaScript, no layout cost. Put the mask on the scroll container itself, and the border on a wrapper - the gradient masks whatever it covers, so a border on the same element would fade too. The frame stays crisp; only the content dissolves into the edge. ## Usage Wrap the scroll area in a bordered, `overflow-hidden` frame, then put `scroll-fade` on the inner scrolling element. ```tsx title="notifications.tsx" // Frame owns the border + corners; the inner element owns the scroll + mask.
    {items.map((item) => (
  • {item.title}
  • ))}
``` - `scroll-fade` - `scroll-fade-x` - `scroll-fade-t` - `scroll-fade-b` - `scroll-fade-l` - `scroll-fade-r` ## Horizontal `scroll-fade-x` masks the inline edges instead. The right edge fades while there are more chips to reach, and sharpens at the end of the row. ```tsx title="tabs.tsx"
{tags.map((tag) => ( {tag} ))}
``` ## Single edge Reach for a one-sided utility when only one edge should bleed - a chat log that fades into its header (`scroll-fade-t`), or a list that only needs to hint at more rows below (`scroll-fade-b`). The crisp edge never fades, even mid-scroll. ## Customizing Two CSS variables tune the effect per element - set them with an arbitrary property so they cascade cleanly and stay overridable. - `--scroll-fade-size`: min(2.5rem, 12%) - `--scroll-fade-reveal`: 6rem ```tsx title="customizing.tsx" // Deeper fade, quicker reveal.
    {/* … */}
``` ## Browser support The scroll-aware behavior rides [scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline). Where they aren’t supported, a `@supports` fallback keeps a static fade on the utility’s edges, so the hint never disappears - it just stops tracking the scroll position. Because the effect is driven by the user’s own scrolling (it never plays on its own), it’s left on under `prefers-reduced-motion`. --- # Icons Phosphor Icons - a flexible, MIT-licensed family with 6 weights per glyph and over 1 400 icons. Always import from /ssr in Server Components; the bare import is for 'use client' files only. ## Import Phosphor ships two barrels. The `/ssr` barrel is tree-shaken and safe to use in any environment - Server Components, Edge, Node. The bare `@phosphor-icons/react` import registers a React context for the default weight, which requires a client boundary. Default to `/ssr` everywhere and only switch to the bare import inside `"use client"` files that need the context API. ```tsx title="example.tsx" // ✓ Server Components and most files - default to this import { Bell, Star, ArrowRight } from "@phosphor-icons/react/ssr" // ✓ Client Components that need the context API (e.g. global weight) "use client" import { Bell, Star, ArrowRight } from "@phosphor-icons/react" // ✗ Never import the whole package import * as Icons from "@phosphor-icons/react" ``` ## Sizes Icons use Tailwind’s `size-*` utility (shorthand for `width` + `height`). Components apply a default of `size-4` to any unclassed `svg` child - only override when the context calls for a different size. - `size-3`: 12px (tight inline, badge counters) - `size-3.5`: 14px (separators, tag icons) - `size-4`: 16px (default - most components) - `size-5`: 20px (emphasis, standalone labels) - `size-6`: 24px (display, empty states) ## Weights Every Phosphor icon ships in six weights. Pass the `weight` prop - no separate import per style. `bold` is the default in Koala UI - it holds its weight at small sizes and reads as deliberate next to our type. Use `fill` for selected / active states, and drop to `regular` or `light` when you want a softer, more delicate mark. - `thin` - `light` - `regular` - `bold` - `fill` - `duotone` ```tsx title="example.tsx" // Pass weight directly - no separate import {/* default */} {/* selected state */} {/* softer, lighter mark */} ``` ## Color Icons inherit the current text color - set `text-*` on the icon or any ancestor. Never hardcode a hex value; always use a semantic token so the icon re-themes correctly across all four Koala themes. - `foreground` - `muted-fg` - `primary` - `destructive` - `success` - `warning` - `info` ## Accessibility Most icons are decorative - they reinforce meaning already conveyed by adjacent text, so screen readers should skip them entirely. Interactive icons (icon-only buttons, links) need an accessible label so a keyboard or AT user knows what they do. ```tsx title="a11y.tsx" // ✓ Decorative icon alongside text - silence it // ✓ Icon-only - label the interactive element, not the icon // ✓ Icon-only with visible tooltip - sr-only is redundant, but keep aria-label // so AT users get the label without waiting for the tooltip to appear // ✗ aria-label on the icon itself - AT reads "Go to next step" then "image" ``` ## In context Button, Badge, and Breadcrumb all size icons automatically via `[&_svg]` selectors - you rarely need to pass a size class when composing with those components. ```tsx title="usage.tsx" import { ArrowRight, Plus, Check } from "@phosphor-icons/react/ssr" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" // Button sizes the svg to size-4 automatically // Badge sizes the svg to match its own size step Deployed ``` --- # File Icons File-type icons as pure inline SVG: a sheet with a folded corner and the extension, toned by category. Three variants, four sizes, and every icon ready to copy or download. ## File icons 42 common extensions, grouped by category and toned by it: PDFs read red, sheets green, pictures purple. Pick a variant above the grid, then hover an icon and use Copy SVG to paste it straight into Figma, or Download SVG for a standalone file. The export carries the colors of the theme you are viewing. ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add file-icon ``` Manual: run `npm install tailwind-variants tailwind-merge`, then copy the source into `components/ui/file-icon/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Variants Three ways to spend the same tone. `default` is white paper with the tone on a label band, the one FileCard uses. `soft` washes the whole sheet in the tone, quieter for long lists. `solid` makes the sheet the tone, for a single file that leads. The label sits in the same place in all three. - `default`: paper + tone band - `soft`: tone wash - `solid`: tone sheet ## Categories An extension resolves to one of 12 categories, and the category picks the tone. Every tone is a semantic color role, so it re-themes with the palette. Pass `extension` and the category follows; pass `type` alone for a generic label (IMG, VID). Anything unknown falls back to gray. - `pdf`: destructive (PDF) - `image`: purple (Image) - `video`: pink (Video) - `audio`: orange (Audio) - `doc`: info (Document) - `sheet`: success (Spreadsheet) - `slides`: warning (Presentation) - `archive`: teal (Archive) - `code`: teal (Code) - `design`: foreground (Design) - `text`: muted-foreground (Text) - `default`: muted-foreground (Other) ## Sizes `size` sets the height; the width follows the sheet’s 5:6 box. Or size it yourself with a height utility in `className`. - `sm`: 32px (dense lists, table cells) - `md`: 40px (default: rows, attachments) - `lg`: 48px (upload trays, cards) - `xl`: 64px (empty states, previews) ## In context FileCard leads with this icon, so an attachment list gets the category at a glance. Derive the type from the filename and the row needs nothing else. The [FileCard](https://www.koalaui.com/docs/components/file-card.md) page covers progress, states and actions. ## Usage Pass the extension or the whole filename. The icon is decorative (`aria-hidden`) because the name beside it says what the file is; give it an `aria-label` when it stands alone. It holds no state, so it renders in Server Components too. ```tsx title="attachments.tsx" import { FileIcon, FILE_ICON_TYPES, fileTypeFromName } from "@/components/ui/file-icon" // From an extension or a whole filename: the category and tone follow // A category alone stamps its generic label (IMG, VID, ZIP) // Override the stamped text, or the tone // Name the category in a meta line const type = fileTypeFromName(file.name) {FILE_ICON_TYPES[type].name} // "Spreadsheet" ``` --- # Flags 261 country and region flags as pure inline SVG, in three shapes and six sizes. Search the set, pick a shape, and copy or download any flag as a standalone SVG. ## Flags Every country, the territories that fly their own flag, and a few regions such as the European Union, Scotland and Catalonia, sorted by name. Search by name or by code, pick a shape, then hover a flag and use Copy SVG to paste it into Figma, or Download SVG for a standalone file in that shape. ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add flag ``` Manual: run `npm install country-flag-icons tailwind-variants tailwind-merge`, then copy the source into `components/ui/flag/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Shapes One set of 3:2 art, cropped three ways. `rectangle` shows the whole flag; `square` and `circle` cover the box from the centre, so the color always reaches the edge. A 1px hairline sits over the art so white flags like Japan’s keep their edge on a white surface. - `rectangle`: 3:2, the flag whole - `square`: 1:1, centre crop - `circle`: 1:1, round crop ## Sizes `size` sets the height and the shape sets the width, so a rectangle and a circle of the same size share a line. The corners grow with the flag and follow the radius knob. - `xs`: 12px (inline in small text) - `sm`: 16px (select items, menus) - `md`: 20px (default: rows, inputs) - `lg`: 24px (list leads) - `xl`: 32px (cards, headers) - `2xl`: 40px (profiles, hero stats) ## In context A flag leads a row and the name beside it carries the meaning, which is why the flag is decorative by default. CountrySelect, PhoneInput and JobCard use this same round crop. United States 38% Germany 21% Japan 14% Brazil 9% Scotland 4% ## Usage Pass an ISO 3166-1 alpha-2 code (`ES`), or an ISO 3166-2 code for a region with its own flag (`GB-SCT`). The same codes key the `FLAGS` list, so names come along. Give the flag a `label` when it stands alone, as in a language switcher that shows only flags. ```tsx title="countries.tsx" import { Flag, FLAGS, FLAG_BY_CODE } from "@/components/ui/flag" // Rectangle by default, 20px tall // Round, beside its name {FLAG_BY_CODE.JP.name} // Standing alone: give it a name // Every flag, sorted by name {FLAGS.map((flag) => ( ))} ``` The artwork is `country-flag-icons`, MIT licensed. Because `Flag` resolves any code, the whole set ships wherever it renders on the client, as it already does for CountrySelect and PhoneInput. It holds no state, so in a Server Component the art renders on the server and none of it reaches the browser. --- # Logos Placeholder brand logos: fictional companies, each a colored symbol beside an invented wordmark, plus a small set of real marks. Pure inline SVG, no binary assets. ## Logos 48 imagotypes ordered around the hue wheel: a minimalist colored symbol beside its wordmark. Hover a logo and use Copy SVG to drop the symbol straight into Figma (just paste), or Download SVG for a standalone file. The brand color is baked into the export. ## Usage In code, import the set and a brand, then render the symbol alone or the full lockup. Map the whole list for a logo wall. The symbols are decorative, so they are `aria-hidden`; the wordmark carries the accessible name. ```tsx title="logo-wall.tsx" import { PLACEHOLDER_BRANDS, PlaceholderLogo, } from "@/components/ui/placeholder-logos" const [apex] = PLACEHOLDER_BRANDS // The colored symbol on its own // Symbol + wordmark lockup (the default) // A logo wall: map the set
{PLACEHOLDER_BRANDS.map((brand) => ( ))}
``` These are fictional placeholders, never real companies. Each mark is dependency-free inline SVG that paints with its brand color: the one place these step outside the token system, the same documented exception as the OAuth brand marks. Because the symbol uses `currentColor`, the wordmark re-themes with `text-foreground`, so every lockup reads across all four Koala themes. ## Brand logos Sometimes you need a real customer's logo, not a placeholder: say, Spotify or Netflix on a “works with” wall. Here are 57 of the most recognizable brand marks, ready to copy or download in their official colors. Search by name to find one fast. They live apart from the fictional set above because, unlike those, each one is a trademark you do not own. Hover a logo and use Copy SVG to drop the mark straight into Figma, or grab it inline here. The Spotify mark, color baked in and ready to paste: ```tsx title="spotify.svg" ``` In code, the set works like the placeholders: import `BRAND_LOGOS` and render a mark with `BrandLogo`. Each entry carries the brand's domain (the homepage link via `brandHref`) and, when the brand publishes one, a `guidelines` URL, so your UI can always point back to the source. ```tsx title="brand-wall.tsx" import { BRAND_LOGOS, BrandLogo, brandHref } from "@/components/docs/brand-logos" const spotify = BRAND_LOGOS.find((b) => b.name === "Spotify")! // The trademark mark in its official color // Link to the brand, and to its guidelines when published {spotify.name} {spotify.guidelines && Brand guidelines} // A row of real brand logos
{BRAND_LOGOS.map((brand) => ( ))}
``` --- # Announcement The pill above a hero title: a standing label paired with the thing being announced. Single-element like Badge, and it nests a Badge for the announced half without drawing a second border around it. ```tsx Update available Koala UI v11 is here! ``` ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add announcement ``` Manual: run `npm install radix-ui tailwind-variants tailwind-merge`, then copy the source into `components/ui/announcement/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Usage ```tsx title="usage.tsx" import { Announcement } from "@/components/ui/announcement" import { Badge } from "@/components/ui/badge" export function Example() { return ( Update available Koala UI v11 is here! ) } ``` ## Playground The controls come straight from the recipe, so the two style axes are all there is: the surface it sits on, and whether it behaves like a link. Note that `interactive` is not something you normally set, it follows `asChild` on its own. ## Nesting a Badge This is what the pill is for: a standing label plus the thing being announced. Drop a [Badge](https://www.koalaui.com/docs/components/badge.md) in and the recipe handles the nesting. It tightens its padding to 4px on the side the badge sits on, so the chip nests in an even crescent instead of floating with a wide gap on its outer side, and it trades the badge's own hairline for a fill, so you get one stroke around one chip instead of two borders four pixels apart. Works with the badge trailing, leading, or alone. The horizontal padding is decided per edge, by whatever sits on it: bare text gets 16px, an icon or a dot 12px, a nested badge 4px. That is an optical correction, not a scale. Against a fully round cap the curve recedes from the baseline, so a word set at the same distance as a shape reads as drifting into the edge, while a chip or a glyph nests inside the curve and needs no help. The label is held off the chip by 16px as well, not by the row's 8px gap. 8px is the distance between a mark and its own label, a dot and the word it belongs to; the label and the chip are two separate pieces of information, and at 8px they read as one run of text with a box drawn round half of it. A badge next to an icon keeps the 8px: a glyph already reads as separate. ```tsx Update available Koala UI v11 is here! New Density lands in every control Shipped ``` ## Dot and glyph For a one-line announcement there is nothing to pair, so skip the badge: `dot` renders the same small mark a dot Badge carries, in the current text color. A leading icon works too, and the recipe sizes it to 16px. ```tsx Now in private beta Koala UI for Mobile is live 2,600 teams build on Koala ``` ## Over media `variant="glass"` is the pill for a photo or video hero: a translucent white surface with a backdrop blur, sized for the scrim rather than for the theme. A nested badge goes white with it, dot included, because a brand-colored dot clashes over a photograph. ```tsx Now in private beta Series A $61M to scale support ``` ## As a link Pass `asChild` to render the pill as an `` pointing at the changelog. The hover, focus and press affordances follow `asChild` on their own, so a static pill never scales under a cursor that cannot click it, and the hit area is padded out to 40px tall without changing how the pill looks. ```tsx Update available Koala UI v11 is here! Read the release notes ``` ## In a hero Where it earns its keep. The pill is the first child of [HeroContent](https://www.koalaui.com/docs/components/hero.md) (or of `HeroColumn` in a split hero), above the title. It used to be a Hero part, but nothing ties it to one: the same pill sits in a navbar, over a pricing table, or at the top of a changelog entry. ```tsx title="hero.tsx" Update available Koala UI v11 is here! A Design System built for AI-Powered Products {/* … */} ``` ## API reference ### Announcement A single element that forwards every native `
` prop and merges `className` last. The children are the content: a label, a nested `Badge`, an icon, or any combination of them. ### variant `"solid"` (default), the hairline card pill for a normal surface, or `"glass"`, the translucent pill for a photo or video hero. ### data-lead · data-trail Not props: the component reads its own children and writes what sits on each edge, `"text"` or `"shape"`, onto the element for the recipe to match. CSS cannot work this out for itself: `:first-child` counts elements, and a label arrives as a text node, so a selector reads a label plus a badge as a pill holding one lone chip and spaces it as though the words were not there. ### dot Renders a leading dot in the current text color. Use it for the one-line shape; when a badge is nested, let the badge carry the dot instead. ### asChild · interactive `asChild` renders the pill styles onto a single child element (Radix Slot), typically a link, and `Slottable` keeps the dot inside that element so it belongs to the same hit area. `interactive` defaults to whatever `asChild` is and carries the cursor, the hover, the focus ring and the press scale. Set it explicitly only for a pill that is clickable without `asChild`. To hold a linked pill still, reach for `static` instead, which drops the press scale alone: `interactive={false}` would take the focus ring with it. ## FAQ ### When should I use Announcement instead of a Badge? A Badge carries one label. An Announcement pairs two pieces of information: a standing label (“Update available”) and the thing being announced (“Koala UI v11 is here!”), and the pill is what pairs them. If you only have the second half, a single dot Badge is the lighter choice and the pill adds nothing. ### And instead of a Banner? Banner is the full-bleed bar across the top of a page, sized to interrupt. Announcement is an inline pill that sits inside a composition, usually above a hero title. Same message, different furniture: one takes the whole viewport width, the other takes only the room its text needs. ### Why does a nested badge lose its border? A dot Badge is a transparent chip with a hairline stroke. Inside the pill, which already has one, that paints a second hairline of the same color four pixels in from the first, which reads as a box in a box. The recipe swaps the stroke for a fill one step off the pill's own surface, so the chip stays distinct with a single border around the pair. ### Can I nest a filled badge, like a soft success one? Yes. The recipe sets the nested surface, so a tinted badge would come out muted; pass the fill back on the badge itself (className="bg-success/10") and it wins, because className is merged last. The house convention for marketing pills is a dot badge, though. ### Does it re-theme? The solid variant does: it is built from card, border and muted, so it follows light, dark, cream and moonlight. The glass variant deliberately does not. It only ever sits on darkened media, so it is fixed white-on-scrim, the same documented exception the hero's gradient scrim takes. --- # Banner A full-bleed announcement bar for promos, release notes and site-wide notices. Soft tones tint the background, message, icon and action from one semantic token, so the whole bar reads as a single hue. ```tsx New: cream and moonlight themes just landed. Check it out ``` ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add banner ``` Manual: run `npm install radix-ui tailwind-variants tailwind-merge`, then copy the source into `components/ui/banner/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Usage ```tsx title="usage.tsx" import { Banner, BannerIcon, BannerContent, BannerAction, } from "@/components/ui/banner" export function Example() { return ( New: cream and moonlight themes just landed. Check it out ) } ``` ## Variants Soft tones tint the background from a single semantic token and color the icon, action, and message to match. The message takes a deep, tone-tinted ink (a dark blue, dark purple, ...) on light themes and lifts to a bright tint on dark ones, so it stays readable while the icon and action keep the pure hue. Every tone re-themes across light, dark, cream, and moonlight. ```tsx … … … … … … ``` ## Solid Set `appearance="solid"` for a filled bar. Solid is designed for two theme-stable surfaces: the `default` inverse bar (flips with the theme) and the `brand` accent bar. The soft tones stay tinted because the hue tokens shift lightness per theme. ```tsx Black Friday: 40% off all plans this week. Get the deal Koala UI v1.0 is live. See what's new ``` ## Festive Set `appearance="festive"` for a playful celebration bar: a flat, saturated fill behind white text, the natural home for launches, milestones, and holidays. It ignores the tone `variant`. Recolor the bar by overriding one CSS variable, `--banner-festive-via`, with a brand or flag color. Keep it deep and saturated so the white text stays legible across every theme. ```tsx We just shipped v1.0. Thanks for celebrating with us. See what's new // Recolor the bar with any deep tone Drive the bar with your own color. Join the celebration ``` ## Interactive (confetti) Add `confetti` to a festive bar to make it playful: it bursts once on mount and again on every click. The icon pops on the mount burst only, so clicking showers confetti while the icon stays still. The pieces are themed by the `--banner-confetti-*` tokens, which derive from the festive palette, so they re-tint with it (recolor the bar and the confetti follows). It is decorative and fully honors reduced-motion. Click the bars below. ```tsx We just crossed 9,000 stars. Tap to celebrate! Star the repo // Same interaction, recolored (--via fills the bar, --to accents the confetti) Drive the bar and the confetti with your own colors. Join in ``` ## Ornaments Drop a `BannerOrnament` in to peek a decorative flag, mascot, or emoji in from an edge. It clips to the bar and sits behind the message, tilting away from its side. Below, two Venezuela flags (rounded 16px, not circular) peek from the corners of a flat solid bar, with the confetti tinted to the flag's yellow and red. The flag itself is any element you like, sized and rounded however you want. ```tsx import VE from "country-flag-icons/react/3x2/VE" We're celebrating with Venezuela today. ``` ## Alignment The default `align="center"` centers the message. Switch to `align="between"` to pin the message left and the action right, the natural home for a `Button` CTA. ```tsx You have 14 days left in your free trial. Try the new AI components, free during beta. ``` ## Shape `shape` sets the bar's footprint. The default `"bar"` runs edge to edge, the classic strip atop a page. `"inset"` draws the same bar as a rounded card that sits inside the page gutter, above the navbar: place it in a `SectionContainer` (or any gutter) and it takes that width, its inner row trading the page-width cap for a card interior and a step more height. ```tsx Koala UI now available for Mobile Apps! Check it out ``` ## Dismissible Set `dismissible` to render a trailing close button. Uncontrolled by default (the banner hides itself); pass `open` / `onOpenChange` to control it, and always give a descriptive `dismissLabel`. ```tsx New: cream and moonlight themes just landed. Check it out ``` ## As child action Use `asChild` on `BannerAction` to render the link styles onto a framework link (e.g. Next.js ``) via Radix Slot, keeping client-side navigation. ```tsx import Link from "next/link" Read the v1.0 changelog. See what's new ``` ## API reference | Prop | Type | Default | Description | | --- | --- | --- | --- | | variant | default \| brand \| purple \| pink \| teal \| orange \| info \| success \| warning \| destructive | default | Tone of the bar. | | appearance | soft \| solid \| festive | soft | Fill style. solid supports default (inverse) and brand; festive is a flat celebration bar themed by the --banner-festive-via var. | | align | center \| between | center | Centered message, or message-left / action-right. | | shape | bar \| inset | bar | Edge-to-edge strip, or a rounded card inside the page gutter. | | dismissible | boolean | false | Render a trailing close button. | | confetti | boolean | false | Burst confetti on mount and on click (pairs with festive); honors reduced-motion. | | open | boolean | - | Controlled visibility. | | defaultOpen | boolean | true | Uncontrolled initial visibility. | | onOpenChange | (open: boolean) => void | - | Fires when dismissed. | | dismissLabel | string | Dismiss | Accessible label for the close button. | | className | string | - | Merged onto the root, last. | Parts: `Banner`, `BannerIcon`, `BannerContent`, `BannerAction` (accepts `asChild`), and `BannerOrnament` (decorative edge peek, takes `side`). Each forwards its native props and merges `className` last. ## FAQ ### When should I use a Banner instead of an Alert? Banner is a site-wide, full-bleed announcement bar - promos, release notes, maintenance notices - typically pinned above the navbar. Alert is an inline, in-page status message scoped to a section or form. If it spans the whole viewport width and speaks for the whole site, it's a Banner. ### How do I keep a banner dismissed across reloads? Control it: render the banner with open/onOpenChange, persist the dismissed state yourself (localStorage, a cookie, or user settings), and read that value to set the initial open. The built-in uncontrolled mode only remembers within the current page session. ### Will the tones adapt to dark and the other themes? Yes. Every tone tints from a semantic token (bg-/10) and colors the icon/action from the same role, so banners re-theme automatically across light, dark, cream, and moonlight. The message text stays foreground for contrast. ### Can the action be a client-side route? Yes: set asChild on BannerAction and pass a single link element (a Next.js , for instance). The action renders its styles onto that element via Radix Slot, so you keep client-side navigation and valid markup. ### How do I theme the festive bar (brand or flag colors)? Override --banner-festive-via on the banner (a className or an inline style), or globally on a parent for a consistent palette. That single var fills the flat bar; --banner-festive-to accents the confetti alongside it. The default is a clean indigo chosen so white text holds contrast across all four themes; keep your custom color similarly deep and saturated. The festive bar ignores the tone variant, so this var is the only color source. --- # Bento An asymmetric marketing grid of feature tiles. Named parts you assemble: a tinted icon, a balanced title, a description and a media region. Each tile claims a size and a tone, and the grid collapses to one column. ```tsx {/* Top row: two wide tiles. Every shot has a light and a dark take. */} Store templates Crafted layouts that highlight product benefits, build trust, and drive conversion. High-converting experience Structures built for maximum conversion. {/* Bottom row: three tiles. */} Maximum personalization Customize the components so each project is unique. Detailed documentation Maintain consistency with extensive documentation. All assets in 1 place Logos, avatars, flags, and icons, ready to drop in. ``` ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add bento ``` Manual: run `npm install radix-ui tailwind-variants tailwind-merge`, then copy the source into `components/ui/bento/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Usage ```tsx import { Bento, BentoItem, BentoItemIcon, BentoItemTitle, BentoItemDescription, BentoItemImage, BentoItemMedia, } from "@/components/ui/bento" // Your own screenshots, one per theme. import storeLight from "@/public/shots/store-light.webp" import storeDark from "@/public/shots/store-dark.webp" Store templates Crafted layouts that drive conversion. {/* A screenshot that peeks up from the bottom edge, in the page's theme. */} ``` ## Sizes Each `BentoItem` picks a `size`: `sm` (2 cols), `md` (3 cols), `lg` (4 cols), `feature` (4 cols × 2 rows, the anchor tile), and `full` (the whole row). Below the `lg` breakpoint the grid collapses to two columns, then one, so every tile stays legible on mobile. ```tsx … … … … ``` ## Collapse `collapse` on `Bento` picks where the grid leaves one column. The default, `sm`, steps through two columns before the six. `lg` holds one column until `lg`: reach for it when a row holds three tiles, because a two-column step can only lay a row of three out as a pair and an orphan. The tiles read the choice from the grid, so a wide tile never forces a second column into a one-column grid. Narrow the window to see the difference. ```tsx {/* 2 / 3 / 2: one column until lg, never a pair and an orphan. */} … … … … … … … ``` ## Image fit `imageFit` decides what a `BentoItemImage` is. `frame` (default) floats the shot in a window: inset by the tile padding, top corners rounded, lifted off a muted ground. `bleed` drops the window: the art is larger than the tile, pinned to the copy's left edge, and runs off the right and bottom edges, dissolving through the `fade-b` mask into whatever ground the theme gives it. Set it once on `Bento`; a tile's own `imageFit` wins. ```tsx Product Design Templates Create concepts with over 200 templates. {/* Wider than the tile, anchored top-left, dissolving at the foot. */} {/* A tile's own imageFit beats the grid's. */} Framed, for contrast The same art in the default window. ``` ## Light and dark shots A screenshot is taken in one theme, and a framed window is the first thing that gives it away: a white shot on a dark page reads as a mistake. Pass the same shot taken in a dark theme as `darkSrc` and `BentoItemImage` shows `src` on the light themes (light, cream) and `darkSrc` on the dark ones (dark, moonlight). The swap is the `dark:` variant, so it follows the nearest theme, a pinned band included, and only the shot on screen is downloaded. Every other prop applies to both. ```tsx {/* One tile, two takes of its shot. */} High-converting experience ``` ## Live art A shot is one take of one moment. When the moment should follow the visitor's theme and accent, move, or answer a click, put the real components in the window instead. `BentoItemArt` is the same art window a `BentoItemImage` paints into, framed by default and bled under `imageFit="bleed"`, with your composition inside. Position it yourself: the window clips it, and a bled window is wider than the tile, so anything past the tile's edge is cut there. ```tsx Real components, not a picture The window holds live parts. {/* switches, buttons… */} ``` ## Scale `scale` on `Bento` is the board's type and spacing step. `md` (default) is a block among many: 24px tiles, 16px descriptions. `lg` is the landing board a home page tells its feature story with: 32px tiles, a 32px step to the art, and 18px descriptions. The title stays 8px from its description at both steps, and the bleed cancels the bigger padding. Set once on the grid, so a board never mixes steps. ```tsx Product Design Templates Create concepts with over 200 templates. High-converting experience Structures created for maximum conversion. ``` ## API reference ### Bento The grid container: one column on mobile, two at `sm`, six at `lg`. `collapse` is `"sm"` (default) or `"lg"`, which skips the two-column step; `imageFit` sets the default for every tile; `scale` is `"md"` (default) or `"lg"`, the landing step. Forwards native `
` props and `className`. ### BentoItem One tile. `size` is `"sm"` (default), `"md"`, `"lg"`, `"feature"`, or `"full"`; `tone` is `"brand"` (default), `"purple"`, `"teal"`, `"orange"`, or `"pink"` and tints the icon; `imageFit` is `"frame"` (default) or `"bleed"` and overrides the grid's. Pass `asChild` to render the whole tile as a link. ### BentoItemIcon · BentoItemTitle · BentoItemDescription The tinted icon chip (pass one Phosphor icon as the child, tone inherited from the tile), the balanced `

` title, and the muted `

` description. ### BentoItemImage A screenshot clipped at the tile's edge: a framed window peeking up from the bottom, or bleeding art with `imageFit="bleed"`. A plain lazy `` that fills the window, so it runs in Next, Vite or any React app: pass `src` (a URL or a static image import) and `alt`, plus `darkSrc` for the dark themes' take of the same shot; its height (and the bleed's width) follows the tile `size` and is overridable via `frameClassName`. Children are extra layers inside the window (a floating panel over the shot), positioned by you and clipped and dissolved with the art. ### BentoItemArt The art window with live children instead of a picture: framed or bled by the tile's `imageFit`, with the same height (and bleed width) per `size` as `BentoItemImage`. Position the composition inside it yourself; the window clips it. Forwards native `

` props and `className`. ### BentoItemMedia A freeform bottom-aligned region (`mt-auto`) for anything that isn't a peeking image: a stat, a swatch row, or custom markup. Media edges line up across tiles in a row. Composed first, it flips: the visual sits at the top and the title and description settle on the bottom edge. ## FAQ ### How do I make the asymmetric layout? Give each BentoItem a `size`. The grid is six columns at the `lg` breakpoint, so `feature` (4×2), `sm` (2), `md` (3), `lg` (4), and `full` (6) tile together into a bento. The default arrangement pairs one `feature` tile with two `sm` tiles and a `full` closing band. ### Where do the visuals inside the tiles come from? You provide them. Use `BentoItemImage` for a screenshot that peeks up from the bottom edge (a lazy img that fills the window, so pass src and alt), or `BentoItemMedia` for any freeform bottom-aligned visual: a Chart, a Stat, or custom markup. The board above shows the library itself: each shot is taken from a real page of these docs. Keep purely decorative visuals aria-hidden. ### How do I keep a screenshot in the page's theme? Shoot it twice, once in a light theme and once in a dark one, and pass the second as `darkSrc`. The light take shows on light and cream, the dark take on dark and moonlight, and only the one on screen loads. Anchor the side that matters with `className` (`object-left-top`, `object-right-top`): the window's height is fixed, so a narrow tile crops the shot at the sides. ### Can a whole tile be a link? Yes. Pass `asChild` to `BentoItem` and render an `` (or a Next ``) as its child; the link covers the full surface. The tile stays static on hover, like every in-flow card. ### Why does my 2 / 3 / 2 board leave a tile alone on a tablet? The default grid steps through two columns at `sm`, and a row of three can only fill two columns as a pair and an orphan. Pass `collapse="lg"` to `Bento` to hold one column until the six-column layout takes over. ### How do I make the art run off the tile? Use `imageFit="bleed"`, on `Bento` for every tile or on one `BentoItem`. The window loses its border, ground and shadow, grows wider than the tile, anchors the art to the top-left, and fades it out at the foot with the `fade-b` mask, so it dissolves into any theme's ground. ### What does tone change? Only the icon chip's tint. It maps to the theme's hue roles (brand, purple, teal, orange, pink) as a soft 10% background with a solid foreground, so it reads on every theme. ### Is it responsive? Yes. The grid is one column on mobile, two at `sm`, and six at `lg`, and every `size` resolves its spans only at `lg`, so tiles stack cleanly on small screens. --- # FAQs A marketing-ready frequently-asked-questions section built on the Accordion. It pairs a header with a single-expand question list and an optional contact CTA, in a centered stack or a sticky two-column split. ```tsx Frequently asked questions Everything you need to know about the product and how it works. The core library is free under a permissive license. A paid PRO tier adds marketing sections, full pages, and priority support. Koala UI targets React 19 and the Next.js App Router out of the box, and drops into any React setup running Tailwind CSS v4. Yes. Every component is built on semantic tokens and tailwind-variants, and accepts a className that merges last, so you can re-theme globally or override one instance. Run npx koalaui-cli@latest add <component>, or copy the source straight from the docs. There is no runtime package to lock you in. ``` ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add faqs ``` Manual: run `npm install radix-ui tailwind-variants tailwind-merge`, then copy the source into `components/ui/faqs/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Usage ```tsx title="usage.tsx" import { Faqs, FaqsHeader, FaqsTitle, FaqsDescription, FaqsList, FaqsItem, FaqsFooter, } from "@/components/ui/faqs" export function Example() { return ( Frequently asked questions Everything you need to know. The core library is free under a permissive license. ) } ``` ## Stacked layout The default `layout="stacked"` centers the header above a width-constrained list. It is the classic centered FAQ, ideal at the foot of a landing page. Set a `defaultValue` on `FaqsList` (matching a `FaqsItem` value) to open the first answer so the section never reads as an empty stack of triggers. ```tsx Frequently asked questions Short answers to the questions we hear most often. A Figma library mirroring the token set ships with the PRO tier, kept in sync with each release. Re-run the CLI to pull the latest source for any component, or watch the changelog for release notes. Yes. Every component re-themes across light, dark, cream, and moonlight from a single set of semantic tokens. ``` ## Split layout Set `layout="split"` for a two-column grid: a header column that stays `sticky` while the list scrolls beside it (the Stripe / Linear FAQ). The columns stack on mobile, and `FaqsFooter` spans both columns underneath. ```tsx Billing and plans Short answers about pricing, seats, and renewals. Yes. Upgrade or downgrade at any time from your dashboard, and changes are prorated to the day. Team and Enterprise plans include shared seats with centralized billing and role-based access. Your workspace moves to the free tier. Nothing is deleted, and you can upgrade whenever you are ready. Still have questions? ``` ## Grouped by topic For a long, categorized help center, slot `FaqsGroup`s between the header and footer. Each takes a `title` (an `

` under the section `

`) and an optional `description`, then wraps its own questions, so the page reads header → topic → questions → topic. Each group owns its own list, so single-expand is scoped to the topic and the hairline dividers reset at every boundary. It defaults to the borderless `minimal` list, and still forwards every `FaqsList` prop (`variant`, `defaultValue`, `iconPosition`, …). ```tsx const TOPIC_FAQS = [ { topic: "Most asked", questions: [ { q: "How is Koala UI different from other UI kits?", a: "…" }, { q: "Who is it made for?", a: "…" }, ], }, { topic: "The product", questions: [ { q: "How many themes ship with the design system?", a: "…" }, { q: "How often is Koala UI updated?", a: "…" }, ], }, // … ] Have any questions? We've got answers The questions we hear most, grouped by topic so you can jump straight to the one you need. {TOPIC_FAQS.map((group, gi) => ( {group.questions.map((item, i) => ( {item.a} ))} ))} ``` ## Landing scale `size="lg"` on `Faqs` is the landing-page FAQ: topic headings big enough to scan the page by, and every list inside defaults to the Accordion's own `lg` step (an 18px semibold question on a taller row), so questions and topics step up together. The first topic sits flush, because a landing FAQ usually has its lede in a `SectionHeader` above the block. Pair it with `marker="plus"`. ```tsx {TOPIC_FAQS.map((group) => ( {group.questions.map((item, i) => ( {item.a} ))} ))} ``` ## List style `FaqsList` forwards straight to the Accordion, so its `variant` controls the chrome: `card` (the default: one bordered box with shared dividers), `minimal` (borderless rows divided by hairlines), or `separated` (each item its own bordered box). None of those three paints a fill, so the list takes the ground it sits on, whether that is the page or a muted band. The fourth, `pill`, is the chip list: a closed question hugs its own text and the open one floods into a filled card, for a short FAQ sitting beside a picture rather than filling the measure on its own. ```tsx {/* Default: one bordered box */} … {/* Borderless rows */} … {/* Each item its own card */} … {/* Chips: closed questions hug their text */} … ``` ## Helpful feedback For a help-center / knowledge-base FAQ, set `iconPosition="leading"` on `FaqsList` to move the caret ahead of the question, and add `feedback` to a `FaqsItem` to mount a Helpful / Not helpful voting row beneath its answer. The chosen pill stays emphasized while the other dims, and an acknowledgement fades in. For full control (custom labels, an `onVote` handler), drop a `FaqsFeedback` into the answer yourself. ```tsx At our agency, we take pride in our distinctive personalized approach, meticulous attention to detail, and our unwavering commitment to delivering unparalleled designs that surpass client expectations. Most residential projects run six to twelve weeks from brief to install, depending on scope and lead times on custom pieces. Absolutely. We design around the pieces you love and fill the gaps, so the result feels collected rather than bought all at once. ``` ## Footer call to action `FaqsFooter` is an optional closing row for a contact prompt. Drop a line of copy beside one of our `Button` components; it centers in the stacked layout and spans both columns in the split layout. ```tsx Frequently asked questions Email the team or open a thread from your dashboard. We reply within one business day. Open an issue from the docs footer and include a reproduction; we triage daily. Can't find what you're looking for? ``` ## Composition The parts are plain composition, so a list of questions maps cleanly into `FaqsItem`s. Give each one a stable `value` when you want to target it with `defaultValue`; omit it and a unique id is generated for you. ```tsx const PLAN_FAQS = [ { q: "Can I change plans later?", a: "Yes, changes are prorated to the day." }, { q: "Do you offer team seats?", a: "Team and Enterprise plans include shared seats." }, // … ] Billing and plans {PLAN_FAQS.map((item, i) => ( {item.a} ))} ``` ## API reference | Prop | Type | Default | Description | | --- | --- | --- | --- | | Faqs.layout | stacked \| split | stacked | Centered header above the list, or a sticky two-column grid. | | Faqs.size | md \| lg | md | Type scale. lg is the landing FAQ: bigger topic headings, and every list inside defaults to the Accordion's lg step. | | FaqsList.variant | card \| minimal \| separated | card | Forwarded to Accordion: the list chrome. Borders only, never a fill. | | FaqsList.type | single \| multiple | single | Forwarded to Accordion: one open answer, or many. | | FaqsList.collapsible | boolean | true | Forwarded to Accordion (single only): allow closing the open item. | | FaqsList.defaultValue | string \| string[] | - | Forwarded to Accordion: the item(s) open on mount. | | FaqsList.density | comfortable \| compact | comfortable | Forwarded to Accordion: row height and content inset. | | FaqsList.iconPosition | leading \| trailing | trailing | Forwarded to Accordion: caret before or after the question. | | FaqsGroup.title | ReactNode | - | Topic heading (an

) above the group's questions. | | FaqsGroup.description | ReactNode | - | Optional supporting copy under the topic heading. | | FaqsGroup.* | FaqsList props | variant="minimal" | Forwarded to the group's own FaqsList (variant, defaultValue, …). | | FaqsItem.question | ReactNode | - | The question shown in the trigger row. | | FaqsItem.value | string | auto | Unique item id. A stable id is generated when omitted. | | FaqsItem.feedback | boolean \| FaqsFeedbackProps | false | Mount a Helpful / Not helpful voting row beneath the answer. | | FaqsFeedback.reveal | "always" \| "hover" | "always" | `hover` opens the row with its answer instead of showing it permanently, for the always-open reading model where every answer is visible at once. The space is not reserved, so no empty gap sits under each answer; it still opens on keyboard focus, stays open once a vote is cast, and opts out entirely on touch, which has no hover. | | FaqsFeedback.onVote | (v: "up" \| "down") => void | - | Called with the chosen vote when the reader rates the answer. | | className | string | - | Accepted by every part, merged last. | Parts: `Faqs`, `FaqsHeader`, `FaqsTitle`, `FaqsDescription`, `FaqsGroup`, `FaqsList`, `FaqsItem`, `FaqsFeedback`, `FaqsFooter`. `FaqsList` forwards all `Accordion` props; each part forwards its native props and merges `className` last. ## FAQ ### How is FAQs different from Accordion? FAQs is a marketing section built on top of Accordion. Accordion is the generic disclosure list; FAQs adds the section composition around it - a header (title + description), the Q&A list with sensible marketing defaults, an optional contact CTA, and a stacked or split layout. Reach for Accordion for in-app disclosure, and FAQs for a landing-page question section. ### Do I have to give every FaqsItem a value? No. FaqsItem generates a stable, SSR-safe id when you omit value. Pass an explicit value only when you want to open it via the list's defaultValue, or otherwise target it. ### Can more than one answer be open at once? Yes. FaqsList forwards to Accordion, so set type="multiple" (and drop collapsible, which only applies to single) to let several answers stay open together. ### How do I split a long FAQ into topic sections? Use FaqsGroup. Drop one per topic between FaqsHeader and FaqsFooter, give it a title (and optional description), and nest its FaqsItems inside. Each group is its own list, so single-expand is scoped to the topic. That is the grouped / help-center variant: header → topic → questions → topic. ### How do I make the contact prompt live in the header instead of the footer? Because the parts are plain composition, just place your prompt and Button inside FaqsHeader rather than using FaqsFooter. In the split layout that keeps the call to action in the sticky left column beside the title. --- # Footer A composable site footer for marketing, product and ecommerce pages. Pure layout: a brand column plus grouped link columns up top, and a bottom bar with copyright, legal links and social icons. ```tsx
The commercial React design system… Features Pricing {/* …more columns */} © 2026 Koala UI. All rights reserved. Privacy Terms
``` ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Add this component (its dependencies come along) npx koalaui-cli@latest add footer ``` Manual: run `npm install radix-ui tailwind-variants tailwind-merge`, then copy the source into `components/ui/footer/` and adjust the import paths to your project. Components pull `cn` from `lib/utils`, the `tv` wrapper from `lib/tv`, and (for multi-part components) `createContext` from `lib/create-context`. ## Usage ```tsx title="usage.tsx" import { GithubLogo, XLogo } from "@phosphor-icons/react" import { Footer, FooterTop, FooterBrand, FooterTagline, FooterSocial, FooterSocialLink, FooterColumns, FooterColumn, FooterLink, FooterBottom, FooterCopyright, FooterLegal, } from "@/components/ui/footer" export function Example() { return (
Koala UI Tools for teams that ship every week. Features Pricing About Careers © 2026 Koala UI. All rights reserved. Privacy Terms
) } ``` ## With newsletter A newsletter signup is just an [Input](https://www.koalaui.com/docs/components/input.md) and a [Button](https://www.koalaui.com/docs/components/button.md) composed inside `FooterBrand`. The footer stays dependency-free. ```tsx Get product updates. No spam. ``` ## Wide link grid `layout="wide"` hands the whole top row to the link columns, for footers that lead with a deep sitemap instead of a brand statement. Drop `FooterBrand` and the columns distribute edge to edge. ```tsx
… … … … … … © 2026 Koala UI. All rights reserved. …
``` ## Brand above the grid The brand does not have to sit beside the columns. Stack `FooterTop` and the lockup leads the footer with the lede opposite it, the grid runs underneath, and the social row moves down into the bottom bar. ```tsx
Subscribe now so you don't miss… {/* …six columns */} © 2026 Koala UI. All rights reserved. …
``` ## Bottom bar only For simple pages, drop the columns and render just the `FooterBottom` bar: the brand, an inline nav, and the copyright on one line. ```tsx
Features Pricing Docs Blog Contact © 2026 Koala UI
``` ## Density `density` tunes the block padding and the gap to the bottom bar. Set it per footer or for a whole subtree with `DensityProvider`. See [Density](https://www.koalaui.com/docs/foundations/density.md). ```tsx
…
``` ## API reference ### Footer The root `