# Koala UI documentation > A commercial React design system: 151 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. > 46 foundation components are free; the other 105, the sections, the page > examples and the templates come with Koala UI Pro. Every docs page (guides, foundations and components; a Pro component with its API and usage, never its source) 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: the 46 foundation components (buttons, form controls, overlays, menus, tabs, feedback), the lib helpers and the design tokens. Their docs, code included, are below. - Pro, with a license: the other 105 components, 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 Pro component's page documents its API and usage, and a section page's markdown names its Pro variants; their source is never published in these docs. Tell the user an item is Pro instead of rebuilding it. --- # 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 items The foundation components, the lib helpers and the tokens are free. The other components, 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 component and 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. --- # Existing members Bought Koala UI on the old site? Your purchase carries over. Find your license key, unlock the site with it, and install the Pro source with the CLI. The old site signed members in with an email and a password. This one has no accounts: **your license key is your login**. It is the same key Lemon Squeezy sent when you bought, so there is nothing to buy again and nothing to migrate. Licenses issued byLemon Squeezy ## Get access 1. **Find your license key** Lemon Squeezy emailed it with your receipt. Can’t find the email? Open [your orders](https://app.lemonsqueezy.com/my-orders) and sign in with the email you paid with: every order shows its key. It looks like this, behind the same lemon as the log-in field: `38b1460a-5104-4067-a91d-77b872934d51` 2. **Unlock the site** Go to [/account](https://www.koalaui.com/account) and enter the key with your purchase email. The Code tab of every Pro section then shows its files. It uses no CLI seat, so unlock as many browsers as you like. 3. **Install with the CLI** Activate the key once per machine, then add Pro sections, pages and templates like any free component. The full setup is in [Installation](https://www.koalaui.com/docs/installation.md). ```bash npx ``` 4. **Open the Figma file** Nothing changes here. The Figma library is still in your receipt email and in [your orders](https://app.lemonsqueezy.com/my-orders), with every update. ## What your purchase includes A paid Koala UI license now covers the React kit too: every Pro section, page example and template, on top of the Figma file you bought. Purchases made before October 1, 2026 keep what the old license promised, lifetime updates at no extra cost. See clause 4 of the [license](https://www.koalaui.com/legal/license). ## FAQ ### Where did my old account and password go? They no longer exist. The old log-in and password-reset links now open /account, where the license key and your purchase email replace them. ### I can't find my license key. Sign in to [Lemon Squeezy orders](https://app.lemonsqueezy.com/my-orders) with every email you might have paid with, a work address included. Still nothing? Write to [info.koalaui@gmail.com](mailto:info.koalaui@gmail.com) with the purchase email and the rough date, and we find it for you. ### My key is refused. Check that the email matches the one on the receipt exactly. A key from the free Figma download, or from a refunded order, does not unlock Pro. If neither applies, write to us with the key's last four characters. ### The CLI says my seat is taken. A Solo license runs on one machine at a time. Run `npx koalaui-cli@latest logout` on the old one to free it. Machine gone? Write to [info.koalaui@gmail.com](mailto:info.koalaui@gmail.com) with the license id from /account and we free it. ### Do I need to pay again? No. Koala UI is a one-time purchase that never expires, and your key keeps working on the new site, the CLI and the Figma file. --- # 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 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: the foundation components, the lib helpers and the tokens install anywhere with `npx koalaui-cli@latest add `. The other components, 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 item. 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 | Koala Icons | | **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 | 199 | | Page examples | No | 35 | | Templates | No | 8 | | Updates that keep your edits
`update` replaces the files you did not touch and lists the ones you did. | No | Yes | | **License** | | | | Price
The 46 foundation components are free. The other components, every section, 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 Koala Icons, our own set, 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.** 151 components, 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. The 46 foundations are free, the rest are Koala UI Pro. - **Sections, pages and templates.** 199 sections for marketing, app and store pages, in families (hero, feature, pricing, FAQ, checkout and more), 35 page examples and 8 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 Koala Icons, and the two can live side by side until you swap yours. --- # Changelog What's new in Koala UI, day by day: new components, improvements and fixes. ## October 19, 2026 - Fixed [Textarea](https://www.koalaui.com/docs/components/textarea.md): The resize grip sits in the bordered box's corner, level with the counter, instead of floating above the footer, and is drawn with the new `Notches` Koala icon. - Improved [Dialog](https://www.koalaui.com/docs/components/dialog.md): Clearer rhythm: the header stands apart from the body (and drops that margin when the footer follows), the icon sits closer to the title, the close button centres on the first row, and the bordered footer gets more room. - Improved [Expandable Search](https://www.koalaui.com/docs/components/expandable-search.md): The docs header search is now an Expandable Search: it rests as a magnifier, opens on click and lists matches right under the field. ⌘K still opens the command palette. - Fixed [Accordion](https://www.koalaui.com/docs/components/accordion.md): Every disclosure panel (Accordion, Collapsible, Tool, Plan, Tree, Sidebar groups) keeps its children's edge and focus rings painted while it opens and closes. - Fixed [Paper](https://www.koalaui.com/docs/components/paper.md): A section band no longer doubles the rule above it. - Improved [Paper](https://www.koalaui.com/docs/components/paper.md): A `SignaturePad` inside `PaperSignature` drops its own box and guide and signs on the sheet's line, scaled with the sheet. - New [Input](https://www.koalaui.com/docs/components/input.md): `InputFloatingField`: the label rests as the placeholder and floats to the top edge on focus, value or autofill. - Improved [Radio Group](https://www.koalaui.com/docs/components/radio-group.md): RadioCard states no longer compete: every state is one ink, keyboard focus adds a soft ink halo, a click shows the ink selection straight away, and hover is a quieter hint. - Fixed [Floating Window](https://www.koalaui.com/docs/components/floating-window.md): A minimized window no longer shows a stray hairline under its bar. - Improved [Floating Window](https://www.koalaui.com/docs/components/floating-window.md): Dragging lifts the window from the grab point with a deeper shadow, and the cursor holds while the pointer leaves the bar. - Fixed [Tool](https://www.koalaui.com/docs/components/tool.md): The first section's top hairline no longer vanishes while the card opens. - Fixed [Voice Roster](https://www.koalaui.com/docs/components/voice-roster.md): A group mark before the avatar keeps the full gap, and the header and overflow line align with the row text at each density. - New [Field](https://www.koalaui.com/docs/components/field.md): `info` on `FieldLabel` and `FieldGroupLabel` puts the help behind an info icon with a tooltip; it still describes the control for screen readers. - New [Empty State](https://www.koalaui.com/docs/components/empty-state.md): `layout="inline"` puts the parts on one left-aligned line, for an empty list inside a sheet or a panel. - New [Rich Text Editor](https://www.koalaui.com/docs/components/rich-text-editor.md): `imageLinks` shows the picture under any block that links to an image file; the saved HTML keeps just the link. ## October 18, 2026 - Fixed [Layout](https://www.koalaui.com/docs/components/layout.md): The `docked` content keeps its top hairline under a topbar: it sits 1px lower, so the sticky bar no longer paints over the ring. - Improved [TeamMember](https://www.koalaui.com/docs/components/team-member.md): Name and role sit closer together in every layout, so the pair reads as one label. ## October 17, 2026 - Improved [Koala Icons](https://www.koalaui.com/docs/foundations/icons.md): Every component, section and page now draws Koala Icons: 117 new icons bring the set to 487, and `@phosphor-icons/react` is no longer a dependency. - New [Brand logos](https://www.koalaui.com/docs/foundations/icons.md): `@/lib/brand-logos` holds the monochrome brand marks (GitHub, X, LinkedIn, Dribbble, TikTok and more) with the same `size`, `color` and `title` props as an icon. ## October 16, 2026 - Improved [Motion](https://www.koalaui.com/docs/foundations/motion.md): No control scales down on press anymore: Button, inputs, pickers, toggles and every other control answer a press with the focus ring and color alone, and `static` stays for compatibility. - Improved [SplitButton](https://www.koalaui.com/docs/components/button.md): Takes a `menuClassName` for the menu, never shrinks in a flex row, and now drives the docs Copy page menu. - Improved [VoiceRoster](https://www.koalaui.com/docs/components/voice-roster.md): Speaks Koala now: rounded rows on the elevation floor, a soft green fill instead of the spine and wash, body type instead of mono, round group dots, the supervisor star after the name, and `panel` rows sit on the page's own ground like a List. - Improved [Hero Section 1](https://www.koalaui.com/marketing/sections/hero-section-1): A `rotate` list turns the end of the headline into a rotating highlight, the same wash as `HeroHighlight`. - Fixed [HeroHighlight](https://www.koalaui.com/docs/components/hero.md): A rotating highlight now server-renders only its first phrase, so search engines read a clean headline instead of every phrase run together. - Improved [SwatchGrid](https://www.koalaui.com/docs/components/swatch-grid.md): Swatches are round now, the selection ring takes the swatch's own color, kept opaque and readable on either theme, and hover hints in that color. - New [ease-lift](https://www.koalaui.com/docs/foundations/motion.md): A motion token for physical objects being picked up or selected: a gentle 10% overshoot, softer than `ease-spring`. - Improved [Ink Tray](https://www.koalaui.com/docs/components/ink-tray.md): The tool in hand now lifts on `ease-lift` with a growing shadow, hover only hints at the lift, and the chosen colour rises slightly. - Fixed `cn` and every recipe now let `ease-drawer`, `ease-spring`, `ease-lift` and `ease-sine` in `className` override a component's own easing. ## October 15, 2026 - New [Toast](https://www.koalaui.com/docs/components/toast.md): `appearance: "minimal"` for a compact one-row toast, `countdown` for a hairline that drains with the timer, and `progress` for a determinate bar you advance by raising the same `id`; the first two can be set app-wide on ``. - New [Ink Tray](https://www.koalaui.com/docs/components/ink-tray.md): A whiteboard pen tray: pen, marker, pencil and eraser rise from behind the bar when picked up, and each tool keeps its own colour and stroke size. - Improved [Fields](https://www.koalaui.com/docs/components/field.md): Input, InputGroup, Select, MultiSelect, NativeSelect, CountrySelect and DatePicker are rounded rectangles instead of pills (12, 14 and 16 by size), and Textarea steps down to `rounded-lg`. - Improved [Button](https://www.koalaui.com/docs/components/button.md): A Button inside a `` takes the field corner for its size, so a submit matches the inputs beside it; everywhere else, a Popover form included, it stays a pill. - Improved [OTPInput](https://www.koalaui.com/docs/components/otp-input.md): Slots follow the field corners, 12, 14 and 16 by size, so a code entry matches the inputs and the Verify button beside it. ## October 13, 2026 - Improved No comments left in the code you install: components, sections, page examples, templates and the shared `lib` helpers all ship as plain code. - New [Icons](https://www.koalaui.com/docs/foundations/icons.md): Koala Icons grows to 370 glyphs, and every example in the docs now draws with them instead of Phosphor. - Improved [Rating](https://www.koalaui.com/docs/components/rating.md): `icon` takes any icon component, Koala Icons or Phosphor, and so do the icon props of CookieConsent categories and Toolbar overflow items. - Improved [Terminal](https://www.koalaui.com/docs/components/terminal.md): A Windows window now: 8px corners, square seams, a `TerminalTab` that merges into the screen and `TerminalCaption` buttons with close going red on hover. - Improved [PromptInput](https://www.koalaui.com/docs/components/prompt-input.md): One size now: the `size` prop is gone, and every composer is the tighter default, with one 32px control height and 16px icons across the toolbar, send included. - New [PromptInput](https://www.koalaui.com/docs/components/prompt-input.md): `PromptInputBannerApps` and `PromptInputBannerApp`: a row of app logos in the banner under the composer, each a button with a tooltip, for a "connect your apps" strip. - Improved [PromptInput](https://www.koalaui.com/docs/components/prompt-input.md): The banner takes a muted tint so it reads as tucked behind the composer, and its content wraps on narrow screens instead of overflowing. - Improved [ModelSelector](https://www.koalaui.com/docs/components/model-selector.md): Capability glyphs are smaller and quieter, and each explains itself in a tooltip on hover through a new `description` prop. - Fixed [Command](https://www.koalaui.com/docs/components/command.md): The sliding highlight no longer covers a group heading or separator as it glides from one group into the next. - Fixed [Tool](https://www.koalaui.com/docs/components/tool.md): Opening or closing a call no longer shows its sheets cut flat by the height animation; they fade in step with it. - Fixed [Citation](https://www.koalaui.com/docs/components/citation.md): A chip no longer wraps alone onto the next line, and no longer crops the first message of a Chat conversation. ## October 12, 2026 - New [Citation](https://www.koalaui.com/docs/components/citation.md): A chip in an answer that previews its sources on hover and goes to them on click. - New [Tool](https://www.koalaui.com/docs/components/tool.md): One call an agent makes to a tool: its arguments, the result and an approval step when a person has to say yes. - New [ModelSelector](https://www.koalaui.com/docs/components/model-selector.md): A searchable model picker grouped by provider, with each model's capabilities and context window. - New [ContextMeter](https://www.koalaui.com/docs/components/context-meter.md): A small ring for how much of the context window a conversation has used, opening onto what fills it. - New [EventCalendar](https://www.koalaui.com/docs/components/event-calendar.md): Month, week and day views where events drag between slots and resize, from the mouse or the keyboard. - New [CompareSlider](https://www.koalaui.com/docs/components/compare-slider.md): Two pictures in one frame with a divider to drag between them, for a redesign, a retouch or a grade. - New [Button](https://www.koalaui.com/docs/components/button.md): A split button: the main action stays visible and its alternatives sit in an attached menu. - New [Chat](https://www.koalaui.com/docs/components/chat.md): An agent trace of reasoning steps, with an `ink-sweep` pass across the step still in progress. - Improved [Tabs](https://www.koalaui.com/docs/components/tabs.md): `segmented` is now the default variant, with a tighter track. - Improved [EmptyState](https://www.koalaui.com/docs/components/empty-state.md): Illustrated states: a small object that belongs to the job for each empty moment. - Improved [DataTable](https://www.koalaui.com/docs/components/data-table.md): An AI generation example that fills an insight column row by row. - Improved [ActivityFeed](https://www.koalaui.com/docs/components/activity-feed.md): A workflow example with a compact phase bar for the total time. - Improved Leaner component source: the long explanatory comments are gone from every component, so the files you copy read as plain code. One-line prop docs stay for editor hints. ## October 11, 2026 - New [Combobox](https://www.koalaui.com/docs/components/combobox.md): A text field you type into to filter a list, then pick one option. - New [Meter](https://www.koalaui.com/docs/components/meter.md): A measurement inside a known range, like storage used or a plan quota. - New [NumberField](https://www.koalaui.com/docs/components/number-field.md): A numeric input with minus and plus steppers. - New [Terminal](https://www.koalaui.com/docs/components/terminal.md): A command-line surface whose title bar doubles as a drag handle. - New [StatusBar](https://www.koalaui.com/docs/components/status-bar.md): The thin strip of status items along the edge of an app shell. - New [TabStrip](https://www.koalaui.com/docs/components/tab-strip.md): The browser-style row of open documents across the top of an app shell. - New [DotMatrix](https://www.koalaui.com/docs/components/dot-matrix.md): A 5×5 dot-matrix loader with a light running through it. - New [TabletFrame](https://www.koalaui.com/docs/components/tablet-frame.md): A device that floats over a game, a map or another full-bleed canvas. - New [SwatchGrid](https://www.koalaui.com/docs/components/swatch-grid.md): A grid of pickable samples: a color, or a glyph over a color. - New [FloatingWindow](https://www.koalaui.com/docs/components/floating-window.md): A small movable, resizable window over the app. - New [Tabs](https://www.koalaui.com/docs/components/tabs.md): `variant="segmented"`, the pill-on-track look for switching views. - Improved [Tabs](https://www.koalaui.com/docs/components/tabs.md): The line and folder indicators sit on the bottom edge of the list and stay fully visible. - Improved [DropdownMenu](https://www.koalaui.com/docs/components/dropdown-menu.md): The exit animation is interruptible: reopening mid-close picks up where it was. - Improved [Map](https://www.koalaui.com/docs/components/map.md): Map pins merge the collapsible chip with the new dispatch, fleet and incident symbols. - Improved API tables and anatomy on the docs pages of every component merged this week, including AI panel, Chat, Prompt Input and Rich Text Editor. - Fixed [DotMatrix](https://www.koalaui.com/docs/components/dot-matrix.md): Keyframes that were left open no longer break the animation. - Fixed [Button](https://www.koalaui.com/docs/components/button.md): An icon-only `size="xs"` button stays round like every other size. ## October 10, 2026 - New [ChannelChat](https://www.koalaui.com/docs/components/channel-chat.md): A many-person channel log for team chat or a radio net. - New [VoiceRoster](https://www.koalaui.com/docs/components/voice-roster.md): Who is on a voice channel, lit as they talk. `variant="panel"` sets it inside a page column. - New [Chip](https://www.koalaui.com/docs/components/chip.md): A small pill you act on, the interactive sibling of Badge. - New [Dot](https://www.koalaui.com/docs/components/dot.md): A status light or color swatch. - New [HoldButton](https://www.koalaui.com/docs/components/hold-button.md): Press and hold to confirm a destructive or final action, with controlled holding and keyboard support. - New [Receipt](https://www.koalaui.com/docs/components/receipt.md): A slip of thermal paper with a torn edge. - New [HandheldDevice](https://www.koalaui.com/docs/components/handheld-device.md): The shell of a handheld gadget, with an LCD, keys and a printer. - New [Button](https://www.koalaui.com/docs/components/button.md): `size="xs"`, a 28px tier for dense interfaces. - New [Progress](https://www.koalaui.com/docs/components/progress.md): A timed mode: pass `duration` and the bar runs itself, like an auto-dismiss countdown. - New [Typography](https://www.koalaui.com/docs/foundations/typography.md): `eyebrow` and `eyebrow-mono` label roles. - Improved [Badge](https://www.koalaui.com/docs/components/badge.md): Takes any color, not only the preset variants. - Improved [PushToTalk](https://www.koalaui.com/docs/components/push-to-talk.md): `size="sm"`, `variant="outline"` and a `live` prop for a hotkey handled elsewhere. - Improved [NotebookPage](https://www.koalaui.com/docs/components/notebook-page.md): A red margin, a spiral binding and fields that sit on the rules. Focus shows only the caret unless `focusTint` is set. - Improved [AI panel](https://www.koalaui.com/docs/components/ai-panel.md): Works inside a container, with jump to latest, prompt limits, quick actions and mentions. - Improved [Chat](https://www.koalaui.com/docs/components/chat.md): The conversation keeps following the latest message through resizes. - Improved [List](https://www.koalaui.com/docs/components/list.md): A `bleed` option and `currentStyle` for the active row. - Improved [Banner](https://www.koalaui.com/docs/components/banner.md): A `size` prop. - Improved [Tabs](https://www.koalaui.com/docs/components/tabs.md): A trailing badge sits concentric with its tab. - Improved Focus rings use one brand color everywhere and are never clipped by a scroll container. - Fixed [Dialog](https://www.koalaui.com/docs/components/dialog.md): Focus returns to the opener when there is no trigger, including a menu item that opened it. - Fixed [Tooltip](https://www.koalaui.com/docs/components/tooltip.md): Automatic hints stand down under an explicit Tooltip, so you never get two bubbles. - Fixed [TabStrip](https://www.koalaui.com/docs/components/tab-strip.md): A sortable tab no longer breaks the tab stop. - Fixed [PushToTalk](https://www.koalaui.com/docs/components/push-to-talk.md): A hold interrupted by unmounting or disabling still ends through `onTalkEnd`. - Fixed [PromptInput](https://www.koalaui.com/docs/components/prompt-input.md): Growing with its text no longer moves the thread above it. ## October 4, 2026 - New Koala UI for React 1.0: 125 free components, four themes and the CLI. - New [Paper](https://www.koalaui.com/docs/components/paper.md): A printed document on screen that scales as a whole. - New [NotebookPage](https://www.koalaui.com/docs/components/notebook-page.md): A sheet of notebook paper behind what someone writes. - New [Existing members](https://www.koalaui.com/docs/existing-members.md): The access guide for buyers from the old site. - Improved [RichTextEditor](https://www.koalaui.com/docs/components/rich-text-editor.md): Opt-in text alignment, `starterKit` and `extensions` props, quote and code styles, and translatable labels. - Improved [Lightbox](https://www.koalaui.com/docs/components/lightbox.md): A `loop` prop and translatable labels. - Improved [Carousel](https://www.koalaui.com/docs/components/carousel.md): A `peek` layout that shows the neighbouring slides. - Improved [Navbar](https://www.koalaui.com/docs/components/navbar.md): `collapseAt="xl"` collapses it at the wider breakpoint. - Improved [Chart](https://www.koalaui.com/docs/components/chart.md): Translucent stacked areas. - Fixed [RichTextEditor](https://www.koalaui.com/docs/components/rich-text-editor.md): No longer calls `onChange` on mount. --- # 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 - `border-ring` (`--ring`): Neutral control edge on hover; focus rings use brand ## 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) ## Eyebrow The small tracked caps line that names a group or a section from above it: a sidebar group, a table-of-contents title, a band in a comparison table, a panel’s “Recent activity”. It is a role, not a size, so it ships as one utility from the token layer instead of five classes copied from screen to screen, where the size and the tracking used to drift a step apart at every call site. - `eyebrow`: 12 / 16px (500 · 0.05em) - `eyebrow-mono`: 12 / 16px (400 · 0.08em) `eyebrow` is the sans label of the UI. `eyebrow-mono` is the typed-note voice (a Hero sign-off, a spec or build line); mono is already wide, so it opens less. Both carry the meta ink and sit on `text-xs` with its paired 16px leading. They set the role and nothing else: spacing and alignment stay where the label is used, and any one trait yields to the matching utility on the same element (`font-semibold`, `tracking-wide`, `text-brand`, `text-white/80` on a photo). Never shrink one to 10 or 11px: a caps label smaller than the scale is a Badge. ```tsx title="panel.tsx" // ✓ The role, in one class.

Recent activity

Build 2.4.1 · eu-west // ✓ One trait tuned, the rest of the role kept.

On this page

New in 2.4

// ✗ Hand-rolled: the size and tracking drift one call site at a time.

Recent activity

``` --- # 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) }) ``` ## Lift: weight for objects you pick up `ease-lift` overshoots by about a tenth, enough to give a physical object weight without the bounce of `ease-spring`. Use it when something is picked up or chosen: a tool rising out of a tray, a swatch lifting when selected. Pair the rise with a shadow that grows, so the lift reads as elevation and not just position, and keep hover to a hint of the same move. Never use it on icons, text or a container changing shape: symbols snap, containers settle. The [Ink Tray](https://www.koalaui.com/docs/components/ink-tray.md) is the reference. ```tsx title="tool.tsx" // ✗ 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
``` ## Hover feedback Cards hint that they are tappable with a subtle hover lift. A press never scales anything down: it confirms through the focus ring and a change of color, and the motion the action itself causes. Hover and press the cards below. 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" // ✓ Hover lift, with 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 "koala-icons" 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 transform on a child, like a hover lift, 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 Koala Icons is our own stroke set, drawn on one 24px grid by one spec. Search it, copy any icon as SVG or JSX, and import it as a plain React component. ## Koala Icons 488 icons in 23 categories, and growing. Search by name, tag or category, and switch to Fill to see the solid weight. Hover an icon and use Copy SVG to paste the master into Figma, Copy JSX for the import and the element, or Download SVG for a file. ## Install and import Install the `koala-icons` package and import each icon by name. Every icon is a plain function with no context and no client hooks, so it renders in Server Components with no `"use client"` boundary. Import `optical.css` once, in your root layout, for the small optical size. ```tsx title="Terminal" npm install koala-icons ``` ```tsx title="app/layout.tsx" import "koala-icons/optical.css" ``` ```tsx title="example.tsx" import { Bell, ArrowRight } from "koala-icons" // One module per icon, for setups that don't tree-shake a barrel import { Bell } from "koala-icons/icons/bell" ``` ## Sizes An icon is `1em` by default, so it follows the text beside it. Set a size with Tailwind's `size-*`, or the `size` prop in pixels. Pick it by context: 16 in buttons, 20 in rows led by a text label, 14 in chips. Button, Input and Badge size an unclassed icon for you. - `size-3.5`: 14px (chips and badges) - `size-4`: 16px (buttons, inputs, menus) - `size-5`: 20px (text-label rows, nav items) - `size-6`: 24px (display, empty states) ## Weights Two weights: `regular`, the outline, and `fill`, the solid version for selected and status states. Fill is derived from the outline, so the two never drift. Only icons with a body get it; a line glyph like a check or an arrow keeps its outline. - `Bell`: regular · fill - `Star`: regular · fill - `House`: regular · fill - `Lock`: regular · fill - `CheckCircle`: regular · fill - `Check`: regular · fill ```tsx title="example.tsx" {/* regular, the default */} {/* selected, active */} {/* no body: stays an outline */} ``` ## Optical sizes At 16px and below the stroke gets heavier, and icons with fine inner marks swap to a simpler small drawing. It happens by itself, from the size each icon renders at, once `optical.css` is imported. Without it, the large drawing shows at every size. - `Trash` (14 · 16 · 20 · 24) - `House` (14 · 16 · 20 · 24) - `Info` (14 · 16 · 20 · 24) - `Copy` (14 · 16 · 20 · 24) ## Color Icons draw in `currentColor`, so a `text-*` utility on the icon or any ancestor colors it. Use semantic tokens only, never a raw value, so the icon re-themes across all four themes. - `foreground` - `muted-fg` - `brand` - `destructive` - `success` - `info` ## Accessibility An icon is decorative by default: `aria-hidden="true"`, because the text beside it already says what it means. An icon-only control names the control, not the icon. An icon that carries meaning on its own takes `title` or `aria-label`, which gives it `role="img"`. ```tsx title="a11y.tsx" // Decorative: the label says it, the icon is skipped // Icon-only: name the button, not the icon // Meaningful on its own: give the icon a name // Avoid: a label on the icon inside a labelled control is read twice ``` ## In context Button and Badge size an unclassed icon to their own step, so composing needs no size class: 16px in a button, 14px in a badge. Deployed 3 new ```tsx title="usage.tsx" import { ArrowRight, Bell, Check, Plus } from "koala-icons" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" Deployed 3 new ``` --- # 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 # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add file-icon ``` Pro: `file-icon` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## 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 # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add flag ``` Pro: `flag` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add announcement ``` Pro: `announcement` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## Usage *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## API reference ### Announcement One pill that pairs a label (often a nested `Badge`) with the thing announced. Forwards all native `
` props; accepts `className`, merged last. | Prop | Type | Default | Description | | --- | --- | --- | --- | | variant | "solid" \| "glass" | "solid" | The hairline card pill for a normal surface, or the translucent white-on-scrim pill for a photo or video hero. | | dot | boolean | false | A leading dot in the current text color, for the one-line shape. With a nested badge, let the badge carry the dot. | | asChild | boolean | false | Renders the pill onto its single child (Radix Slot), typically a link; the dot rides inside that element. | | interactive | boolean | asChild | Cursor, hover, focus ring and press. Set it only for a pill that is clickable without `asChild`. | | static | boolean | false | Kept for compatibility; controls no longer scale on press. | | Data attribute | Description | | --- | --- | | data-lead | `"text"` or `"shape"`: what sits on the leading edge, read from the children so the padding can match (a selector cannot see a text node). | | data-trail | `"text"` or `"shape"`: what sits on the trailing edge. | ## 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 v12 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add banner ``` Pro: `banner` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## Usage *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Anatomy ```tsx import { Banner, BannerOrnament, BannerIcon, BannerContent, BannerAction, } from "@/components/ui/banner" ``` ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Size `size` sets the strip's own height and side padding.`"md"` (default) is the page bar, with the 24px page gutter.`"sm"` is a compact strip for a panel, a tablet screen or the head of a card: 12px sides, a 32px row, the type a step down and a smaller close button, and no page-width cap. Use it instead of reaching into the inner row with selectors. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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`. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## API reference ### Banner The full-bleed bar. Children land in an inner row; the close button and confetti layer are drawn for you. Forwards all native `
` props. Every part accepts `className`, merged last. | Prop | Type | Default | Description | | --- | --- | --- | --- | | variant | "default" \| "brand" \| "purple" \| "pink" \| "teal" \| "orange" \| "info" \| "success" \| "warning" \| "destructive" | "default" | The tone. The icon, message and action all carry it. | | appearance | "soft" \| "solid" \| "festive" | "soft" | The fill. `solid` is defined for `default` (inverse) and `brand`; `festive` is a flat celebration bar that ignores the tone. | | align | "center" \| "between" | "center" | A centered message, or message left and action right (stacked on phones). | | shape | "bar" \| "inset" | "bar" | An edge-to-edge strip, or a rounded card inside the page gutter. | | size | "md" \| "sm" | "md" | The page bar (24px sides), or a compact strip (12px sides, 32px row, smaller type and close, no page-width cap) for panels and cards. | | dismissible | boolean | false | Renders a trailing close button. | | dismissLabel | string | "Dismiss" | The accessible label of the close button. | | open | boolean | - | Visibility, controlled. A closed banner renders nothing. | | defaultOpen | boolean | true | Visibility on first render, uncontrolled. | | onOpenChange | (open: boolean) => void | - | Called when the banner is dismissed. | | confetti | boolean | false | A burst on mount (with an icon pop) and on every click of the bar. Pairs with `festive`; off under reduced motion. | | CSS variable | Description | | --- | --- | | --banner-festive-via | The fill of the `festive` bar. Override it on the banner or a parent; keep it deep enough for white text. | | --banner-festive-to | A confetti accent alongside the festive fill. | | --banner-confetti-{1,2,3} | The three confetti colors, derived from the festive stops by default. | | --surface | Set to the fill of the bar, so a nested control blends with it. | ### BannerIcon The leading icon, tinted to the tone. Forwards all native `` props. ### BannerContent The message, a deep tint of the tone. Forwards all native `` props. ### BannerAction The inline call-to-action link, tinted to the tone. Forwards all native `` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | asChild | boolean | false | Renders onto its single child (Radix Slot), such as a Next.js ``. | ### BannerOrnament A decorative flag, mascot or emoji that peeks in from an edge behind the message, clipped to the bar and `aria-hidden`. Forwards all native `` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | side | "left" \| "right" | "left" | The edge it peeks in from; it tilts away from that edge. | | Data attribute | Description | | --- | --- | | data-side | `"left"` or `"right"`. | ## 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 takes a deep tint of the same hue (the tone mixed toward foreground), so it holds 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add bento ``` Pro: `bento` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## Usage *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Anatomy ```tsx import { Bento, BentoItem, BentoItemIcon, BentoItemTitle, BentoItemDescription, BentoItemImage, BentoItemArt, BentoItemMedia, } from "@/components/ui/bento" {/* or , or */} ``` ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## API reference ### Bento The grid: one column on phones, two at `sm`, six at `lg`. Forwards all native `
` props. Every part accepts `className`, merged last. | Prop | Type | Default | Description | | --- | --- | --- | --- | | collapse | "sm" \| "lg" | "sm" | Where the grid leaves one column. `lg` skips the two-column step, for rows of three that would leave an orphan. | | imageFit | "frame" \| "bleed" | "frame" | The default art window for every tile; a tile's own `imageFit` wins. | | scale | "md" \| "lg" | "md" | The type and spacing step of the whole board: 24px tiles, or the 32px landing board with 18px descriptions. | | Data attribute | Description | | --- | --- | | data-collapse | `"sm"` or `"lg"`. | ### BentoItem One tile, a ring-edged card. Works outside a `Bento` too, with the defaults. Forwards all native `
` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | size | "sm" \| "md" \| "lg" \| "feature" \| "full" | "sm" | Columns claimed at `lg` (2, 3, 4, 4 by 2 rows, 6) and the art height. The wide three also span both `sm` columns. | | tone | "neutral" \| "brand" \| "purple" \| "teal" \| "orange" \| "pink" | "neutral" | The tint of the icon chip, nothing else. | | imageFit | "frame" \| "bleed" | - | A floating screenshot window, or art that runs off the tile and dissolves. Overrides the grid's. | | asChild | boolean | false | Renders the tile as its single child (Radix Slot), so the whole card is one link. | ### BentoItemIcon The tinted icon chip; pass one Koala icon. Forwards all native `
` props. ### BentoItemTitle The balanced title, an `

`. Forwards all native `

` props. ### BentoItemDescription The muted description, a `

`. Forwards all native `

` props. ### BentoItemImage A lazy screenshot clipped at the tile edge, in the window `imageFit` picks. Forwards native `` props (`alt`, `sizes`, `srcSet`) to the image; `className` lands on the ``. | Prop | Type | Default | Description | | --- | --- | --- | --- | | src* | string \| { src: string } | - | A URL or a static image import. | | darkSrc | string \| { src: string } | - | The same shot in a dark theme, shown on dark and moonlight. Only the visible one loads. | | frameClassName | string | - | Classes for the art window (its height, the bleed width). | | children | React.ReactNode | - | Extra layers over the image, positioned by you and clipped with it. | | loading | "lazy" \| "eager" | "lazy" | The native loading hint. | | decoding | "async" \| "sync" \| "auto" | "async" | The native decoding hint. | ### BentoItemArt The same art window holding live components instead of a picture. Position the composition yourself; the window clips it. Forwards all native `

` props. ### BentoItemMedia A freeform bottom-aligned region (a stat, a swatch row). Composed first, it flips: the visual on top, the copy on the bottom edge. Forwards all native `
` props. ## 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. `neutral` (the default) is a soft ink tint; the others map 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`, the wide sizes (`lg`, `feature`, `full`) span both `sm` columns, and every `size` takes its real span 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add faqs ``` Pro: `faqs` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## Usage *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Anatomy ```tsx import { Faqs, FaqsHeader, FaqsTitle, FaqsDescription, FaqsGroup, FaqsList, FaqsItem, FaqsFeedback, FaqsFooter, } from "@/components/ui/faqs" ``` ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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`, …). *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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"`. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## API reference ### Faqs The section container; provides `layout` and `size` to every part. Forwards all native `
` props, and every part accepts `className`, merged last. | Prop | Type | Default | Description | | --- | --- | --- | --- | | layout | "stacked" \| "split" | "stacked" | A centered header above a `max-w-3xl` list, or a two-column grid from `lg` with a sticky header column. | | 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. | ### FaqsHeader The header column for the title, description and anything else you drop in. Forwards all native `
` props. ### FaqsTitle The section heading on the shared `section-heading` scale. Forwards all native `

` props. ### FaqsDescription The supporting blurb under the title. Forwards all native `

` props. ### FaqsList The question list: an `Accordion` (Radix `Accordion.Root`) with marketing defaults, every one overridable. Forwards all Accordion props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | type | "single" \| "multiple" | "single" | One open answer at a time, or many. | | collapsible | boolean | true | Lets the open item close again. `single` only. | | variant | "card" \| "minimal" \| "separated" \| "pill" \| "board" | "card" | The list chrome: one contour, hairline rows, one box per item, chips that flood open, or a two-up tile grid. | | size | "md" \| "lg" | - | The Accordion type scale, defaulting to the `Faqs` size; `lg` sets an 18px semibold question on a taller row. | | marker | "caret" \| "plus" | "caret" | The disclosure mark: a caret that flips, or a plus that turns into a cross. | | iconPosition | "trailing" \| "leading" | "trailing" | Mark after the question, or ahead of it (the help-center look). | | density | "comfortable" \| "compact" | "compact" | Row height and content inset. Falls back to the nearest `DensityProvider`. | | defaultValue | string \| string[] | - | The item(s) open on mount, uncontrolled; an array with `type="multiple"`. | | value | string \| string[] | - | Controlled open item(s). | | onValueChange | (value: string \| string[]) => void | - | Called when an item opens or closes; the value shape follows `type`. | | disabled | boolean | false | Disables every item. | | Data attribute | Description | | --- | --- | | data-orientation | `"vertical"`, set by Radix. | ### FaqsItem One question and answer: Radix `Accordion.Item`, `Trigger` and `Content` in one call, with the answer as `children`. Forwards all `AccordionItem` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | question* | React.ReactNode | - | The question shown in the trigger row. | | value | string | auto | Unique item value. A stable `useId` value is generated when omitted; pass one to target it with `defaultValue`. | | feedback | boolean \| FaqsFeedbackProps | false | Mount a `FaqsFeedback` row under the answer: `true` for the defaults, or an object of its props. | | disabled | boolean | false | Stops the item from opening or closing. | | Data attribute | Description | | --- | --- | | data-state | `"open"` or `"closed"`, on the item, trigger and content. | | data-disabled | Present when the item is disabled. | | CSS variable | Description | | --- | --- | | --radix-collapsible-content-height | The answer's measured height, read by the open and close height tween. | ### FaqsGroup A topic cluster for a categorized FAQ: an `

` heading and optional blurb above its own `FaqsList`, so single-expand is scoped to the topic. Every other prop forwards to that list. | Prop | Type | Default | Description | | --- | --- | --- | --- | | title | React.ReactNode | - | The topic heading, rendered as an `

`. | | description | React.ReactNode | - | Supporting copy under the topic heading. | | variant | "card" \| "minimal" \| "separated" \| "pill" \| "board" | "minimal" | The inner list's chrome; groups default to borderless rows. | | className | string | - | Class for the group wrapper (heading and list). | | listClassName | string | - | Class forwarded to the inner `FaqsList`. | ### FaqsFeedback A Helpful / Not helpful vote row: a single-select `ToggleGroup` of two pills, then an acknowledgement. Works anywhere, not only inside `Faqs`; forwards all native `
` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | onVote | (vote: "up" \| "down") => void | - | Called with the chosen vote, e.g. to forward it to analytics. | | reveal | "always" \| "hover" | "always" | `hover` opens the row with its answer on hover or focus instead of reserving space; it stays open once voted and always shows on touch. | | label | React.ReactNode | - | A lede before the buttons, e.g. "Was this helpful?". | | helpfulLabel | React.ReactNode | "Helpful" | Label of the positive pill. | | notHelpfulLabel | React.ReactNode | "Not helpful" | Label of the negative pill. | | acknowledgement | React.ReactNode | "Thanks for your feedback." | Shown once a vote is cast; `null` suppresses it. | | Data attribute | Description | | --- | --- | | data-voted | `"true"` once a vote is cast with `reveal="hover"`; pins the row open. | ### FaqsFooter The closing call-to-action row: centered in `stacked`, spanning both columns in `split`. Forwards all native `
` props. | Prop | Type | Default | Description | | --- | --- | --- | --- | | size | "sm" \| "md" \| "lg" | "md" | Control height imposed on the buttons and fields inside; a control's own `size` still wins. | | Data attribute | Description | | --- | --- | | data-control-size | The imposed control size. | ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Installation ```bash # One-time setup (tokens + lib helpers) npx koalaui-cli@latest init # Activate your Pro license (once per machine) npx koalaui-cli@latest login # Add this component (its dependencies come along) npx koalaui-cli@latest add footer ``` Pro: `footer` comes with a [Koala UI Pro license](https://www.koalaui.com/pro). Its source is not published in these docs. ## Usage *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## Anatomy ```tsx import { Footer, FooterTop, FooterBrand, FooterTagline, FooterSocial, FooterSocialLink, FooterColumns, FooterColumn, FooterLink, FooterBottom, FooterCopyright, FooterLegal, } from "@/components/ui/footer"
``` ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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. *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## 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). *Code included with [Koala UI Pro](https://www.koalaui.com/pro).* ## API reference ### Footer The root `