Skip to main content
Technology & EngineeringUi Components Services371 lines

Ark UI

"Ark UI: state machine-driven components, headless primitives, Select, Combobox, DatePicker, Toast, styling with any CSS framework"

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Ark 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 asChild and 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

  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.

Anti-Patterns

  1. 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.
  2. Importing from the wrong framework package -- @ark-ui/react for React, @ark-ui/vue for Vue. The API shapes differ between frameworks.
  3. Skipping the Positioner part for floating content and using CSS position directly. The Positioner handles viewport collision detection and dynamic placement.
  4. Using uncontrolled inputs inside Combobox.Input and then trying to read the value externally. Use the onInputValueChange callback to track the input value.
  5. Creating multiple createToaster instances when one global instance is sufficient. Multiple instances create separate stacks and can overlap.
  6. Ignoring the collection pattern 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

Get CLI access →