Ark UI
"Ark UI: state machine-driven components, headless primitives, Select, Combobox, DatePicker, Toast, styling with any CSS framework"
Ark UI is a headless component library powered by state machines (via Zag.js). Each component's behavior is modeled as a finite state machine, making interactions predictable, debuggable, and framework-agnostic at the core. Ark UI wraps these state machines in framework-specific bindings for React, Vue, and Solid. Components are completely unstyled, giving you full control over the visual layer using any CSS approach -- Tailwind, CSS Modules, styled-components, Panda CSS, or plain CSS. ## Key Points - State machine-driven: every component behavior is a deterministic state machine - Headless: zero styling opinions, bring your own CSS - Framework-agnostic core with React, Vue, and Solid bindings - Composable parts pattern similar to Radix UI - Full control over rendering via `asChild` and render props 1. **Use `createListCollection`** for Select and Combobox to define items. This helper normalizes the data structure the component expects. 2. **Use `data-[state]` and `data-[highlighted]` attributes** for styling interactive states. These are consistent across all Ark UI components. 3. **Use the `Positioner` part** for floating elements (Select, Combobox, DatePicker). It handles positioning with Floating UI under the hood. 4. **Use `createToaster`** at module level and export it so any component in your app can trigger toasts without prop drilling. 5. **Leverage `DatePicker.Context`** render prop to access computed values like `weekDays` and `weeks` for building the calendar grid. 6. **Use `asChild`** to render Ark UI behavior on your own elements without extra DOM wrappers. 7. **Group related components** by wrapping Ark UI parts into your own design system components. Create a `ui/select.tsx` that pre-styles the Select parts.
skilldb get ui-components-services-skills/Ark UIFull skill: 371 linesArk UI Skill
Core Philosophy
Ark UI is a headless component library powered by state machines (via Zag.js). Each component's behavior is modeled as a finite state machine, making interactions predictable, debuggable, and framework-agnostic at the core. Ark UI wraps these state machines in framework-specific bindings for React, Vue, and Solid. Components are completely unstyled, giving you full control over the visual layer using any CSS approach -- Tailwind, CSS Modules, styled-components, Panda CSS, or plain CSS.
Key principles:
- State machine-driven: every component behavior is a deterministic state machine
- Headless: zero styling opinions, bring your own CSS
- Framework-agnostic core with React, Vue, and Solid bindings
- Composable parts pattern similar to Radix UI
- Full control over rendering via
asChildand render props
Setup
Installation
# React
npm install @ark-ui/react
# Vue
npm install @ark-ui/vue
# Solid
npm install @ark-ui/solid
# Optional: Panda CSS for styling (made by the same team)
npm install -D @pandacss/dev
Import Pattern
// Named imports from framework-specific package
import { Select } from "@ark-ui/react/select";
import { Combobox } from "@ark-ui/react/combobox";
import { DatePicker } from "@ark-ui/react/date-picker";
import { Dialog } from "@ark-ui/react/dialog";
import { Toast } from "@ark-ui/react/toast";
import { Tabs } from "@ark-ui/react/tabs";
import { Accordion } from "@ark-ui/react/accordion";
// Or import from the root (tree-shakeable)
import { Select, Combobox, Dialog } from "@ark-ui/react";
Key Techniques
Select
import { Select, createListCollection } from "@ark-ui/react/select";
import { Check, ChevronsUpDown } from "lucide-react";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Solid", value: "solid" },
{ label: "Svelte", value: "svelte" },
{ label: "Angular", value: "angular" },
],
});
export function FrameworkSelect() {
return (
<Select.Root collection={frameworks} className="w-64">
<Select.Label className="block text-sm font-medium text-gray-700 mb-1">
Framework
</Select.Label>
<Select.Control>
<Select.Trigger className="flex w-full items-center justify-between rounded-lg border bg-white px-3 py-2 text-sm shadow-sm hover:bg-gray-50">
<Select.ValueText placeholder="Select a framework" />
<Select.Indicator>
<ChevronsUpDown className="h-4 w-4 text-gray-400" />
</Select.Indicator>
</Select.Trigger>
</Select.Control>
<Select.Positioner>
<Select.Content className="mt-1 rounded-md bg-white py-1 shadow-lg border z-50">
{frameworks.items.map((item) => (
<Select.Item
key={item.value}
item={item}
className="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-gray-100 data-[highlighted]:bg-blue-50 data-[state=checked]:font-medium"
>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>
<Check className="h-4 w-4 text-blue-600" />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Select.Root>
);
}
Combobox
import { Combobox, createListCollection } from "@ark-ui/react/combobox";
import { useState } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
const allCountries = [
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "uk" },
{ label: "Germany", value: "de" },
{ label: "France", value: "fr" },
{ label: "Japan", value: "jp" },
{ label: "Australia", value: "au" },
];
export function CountryCombobox() {
const [items, setItems] = useState(allCountries);
const collection = createListCollection({ items });
function handleInputChange(details: { inputValue: string }) {
const filtered = allCountries.filter((item) =>
item.label.toLowerCase().includes(details.inputValue.toLowerCase())
);
setItems(filtered.length > 0 ? filtered : allCountries);
}
return (
<Combobox.Root
collection={collection}
onInputValueChange={handleInputChange}
className="w-72"
>
<Combobox.Label className="block text-sm font-medium text-gray-700 mb-1">
Country
</Combobox.Label>
<Combobox.Control className="relative">
<Combobox.Input
className="w-full rounded-lg border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search countries..."
/>
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronsUpDown className="h-4 w-4 text-gray-400" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content className="mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 shadow-lg border z-50">
{items.map((item) => (
<Combobox.Item
key={item.value}
item={item}
className="flex cursor-pointer items-center justify-between px-3 py-2 text-sm data-[highlighted]:bg-blue-50 data-[state=checked]:font-medium"
>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>
<Check className="h-4 w-4 text-blue-600" />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
);
}
DatePicker
import { DatePicker } from "@ark-ui/react/date-picker";
import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
export function EventDatePicker() {
return (
<DatePicker.Root className="w-72">
<DatePicker.Label className="block text-sm font-medium text-gray-700 mb-1">
Event Date
</DatePicker.Label>
<DatePicker.Control className="relative">
<DatePicker.Input
className="w-full rounded-lg border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Select a date"
/>
<DatePicker.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
<Calendar className="h-4 w-4 text-gray-400" />
</DatePicker.Trigger>
</DatePicker.Control>
<DatePicker.Positioner>
<DatePicker.Content className="mt-1 rounded-lg bg-white p-3 shadow-lg border z-50">
<DatePicker.View view="day">
<DatePicker.Context>
{(api) => (
<>
<div className="flex items-center justify-between mb-2">
<DatePicker.PrevTrigger className="p-1 rounded hover:bg-gray-100">
<ChevronLeft className="h-4 w-4" />
</DatePicker.PrevTrigger>
<DatePicker.ViewTrigger className="text-sm font-medium hover:bg-gray-100 px-2 py-1 rounded">
<DatePicker.RangeText />
</DatePicker.ViewTrigger>
<DatePicker.NextTrigger className="p-1 rounded hover:bg-gray-100">
<ChevronRight className="h-4 w-4" />
</DatePicker.NextTrigger>
</div>
<DatePicker.Table className="w-full">
<DatePicker.TableHead>
<DatePicker.TableRow>
{api.weekDays.map((day, i) => (
<DatePicker.TableHeader key={i} className="text-xs text-gray-500 font-normal pb-1">
{day.narrow}
</DatePicker.TableHeader>
))}
</DatePicker.TableRow>
</DatePicker.TableHead>
<DatePicker.TableBody>
{api.weeks.map((week, i) => (
<DatePicker.TableRow key={i}>
{week.map((day, j) => (
<DatePicker.TableCell key={j} value={day}>
<DatePicker.TableCellTrigger
className="flex h-8 w-8 items-center justify-center rounded-full text-sm hover:bg-blue-50 data-[selected]:bg-blue-600 data-[selected]:text-white data-[today]:font-bold data-[outside-range]:text-gray-300"
>
{day.day}
</DatePicker.TableCellTrigger>
</DatePicker.TableCell>
))}
</DatePicker.TableRow>
))}
</DatePicker.TableBody>
</DatePicker.Table>
</>
)}
</DatePicker.Context>
</DatePicker.View>
</DatePicker.Content>
</DatePicker.Positioner>
</DatePicker.Root>
);
}
Toast
import { Toaster, createToaster } from "@ark-ui/react/toast";
import { X } from "lucide-react";
const toaster = createToaster({
placement: "bottom-end",
overlap: true,
gap: 16,
});
export function ToastProvider({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Toaster toaster={toaster}>
{(toast) => (
<Toast.Root
key={toast.id}
className="min-w-[300px] rounded-lg bg-white p-4 shadow-lg border data-[state=open]:animate-slideIn data-[state=closed]:animate-fadeOut"
>
<div className="flex items-start justify-between">
<div>
<Toast.Title className="text-sm font-semibold">{toast.title}</Toast.Title>
<Toast.Description className="mt-1 text-xs text-gray-500">
{toast.description}
</Toast.Description>
</div>
<Toast.CloseTrigger className="p-1 rounded hover:bg-gray-100">
<X className="h-3.5 w-3.5 text-gray-400" />
</Toast.CloseTrigger>
</div>
</Toast.Root>
)}
</Toaster>
</>
);
}
// Usage anywhere in the app:
function SaveButton() {
return (
<button
onClick={() =>
toaster.create({
title: "Saved successfully",
description: "Your changes have been persisted.",
type: "success",
})
}
className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
>
Save
</button>
);
}
Tabs with Styling
import { Tabs } from "@ark-ui/react/tabs";
export function SettingsTabs() {
return (
<Tabs.Root defaultValue="general" className="w-full max-w-lg">
<Tabs.List className="flex border-b">
<Tabs.Trigger
value="general"
className="px-4 py-2 text-sm border-b-2 border-transparent data-[selected]:border-blue-600 data-[selected]:text-blue-600 hover:text-gray-700"
>
General
</Tabs.Trigger>
<Tabs.Trigger
value="security"
className="px-4 py-2 text-sm border-b-2 border-transparent data-[selected]:border-blue-600 data-[selected]:text-blue-600 hover:text-gray-700"
>
Security
</Tabs.Trigger>
<Tabs.Trigger
value="notifications"
className="px-4 py-2 text-sm border-b-2 border-transparent data-[selected]:border-blue-600 data-[selected]:text-blue-600 hover:text-gray-700"
>
Notifications
</Tabs.Trigger>
<Tabs.Indicator className="h-0.5 bg-blue-600 bottom-0 transition-all duration-200" />
</Tabs.List>
<Tabs.Content value="general" className="p-4 text-sm text-gray-600">
General settings content goes here.
</Tabs.Content>
<Tabs.Content value="security" className="p-4 text-sm text-gray-600">
Security settings content goes here.
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4 text-sm text-gray-600">
Notification preferences content goes here.
</Tabs.Content>
</Tabs.Root>
);
}
Best Practices
- Use
createListCollectionfor Select and Combobox to define items. This helper normalizes the data structure the component expects. - Use
data-[state]anddata-[highlighted]attributes for styling interactive states. These are consistent across all Ark UI components. - Use the
Positionerpart for floating elements (Select, Combobox, DatePicker). It handles positioning with Floating UI under the hood. - Use
createToasterat module level and export it so any component in your app can trigger toasts without prop drilling. - Leverage
DatePicker.Contextrender prop to access computed values likeweekDaysandweeksfor building the calendar grid. - Use
asChildto render Ark UI behavior on your own elements without extra DOM wrappers. - Group related components by wrapping Ark UI parts into your own design system components. Create a
ui/select.tsxthat pre-styles the Select parts.
Anti-Patterns
- Fighting the state machine by trying to manually manage open/close state that the machine already controls. Use the component's API props (
onValueChange,onOpenChange) instead. - Importing from the wrong framework package --
@ark-ui/reactfor React,@ark-ui/vuefor Vue. The API shapes differ between frameworks. - Skipping the
Positionerpart for floating content and using CSS position directly. The Positioner handles viewport collision detection and dynamic placement. - Using uncontrolled inputs inside Combobox.Input and then trying to read the value externally. Use the
onInputValueChangecallback to track the input value. - Creating multiple
createToasterinstances when one global instance is sufficient. Multiple instances create separate stacks and can overlap. - Ignoring the
collectionpattern and passing items as raw arrays. Ark UI collections provide consistent item access, filtering, and keyboard navigation.
Install this skill directly: skilldb add ui-components-services-skills
Related Skills
Daisyui
"daisyUI: Tailwind CSS component library, themes, responsive utilities, component classes, customization, semantic color names"
Headless UI
"Headless UI: unstyled accessible components for Tailwind, Dialog, Combobox, Listbox, Menu, Transition, render props, React/Vue"
Kobalte
Kobalte: accessible, unstyled UI component library for SolidJS with WAI-ARIA compliance and composable compound component APIs
Melt UI
Melt UI: headless, accessible UI component builders for Svelte using the builder pattern with full styling freedom
Park UI
Park UI: pre-designed styled components built on Ark UI primitives, available for React, Vue, and Solid with Panda CSS or Tailwind CSS styling
Radix UI
"Radix UI: accessible primitives, Dialog, Dropdown, Popover, Tabs, Toast, composition pattern, styling with className, controlled/uncontrolled"