Ark UI完全ガイド — ヘッドレスUIコンポーネントで自由なデザインと型安全を両立
Ark UIは、Chakra UIチームが開発した、スタイルを持たないヘッドレスUIコンポーネントライブラリです。React、Vue、Solidで同じAPIが使え、完全なアクセシビリティとキーボード操作をサポートし、Panda CSSやTailwindと組み合わせて自由にスタイリングできます。この記事では、Ark UIの基本から実践的な使い方まで徹底的に解説します。
Ark UIとは
Ark UIは、ビジュアルスタイルを持たず、機能とアクセシビリティのみを提供するヘッドレスUIライブラリです。Zag.jsというステートマシンライブラリをベースに構築されており、複雑なコンポーネントロジックを抽象化します。
主な特徴
- ヘッドレスアーキテクチャ - スタイルなし、完全に自由なデザイン
- マルチフレームワーク対応 - React、Vue、Solidで同じAPI
- 完全アクセシブル - WAI-ARIA準拠、スクリーンリーダー対応
- キーボード操作 - 全コンポーネントがキーボードで操作可能
- 型安全 - TypeScriptで完全な型定義
- ゼロランタイム可能 - Panda CSSと組み合わせでゼロランタイムCSS
- 状態管理内蔵 - 複雑な状態ロジックが組み込み済み
- コンポジション重視 - 小さなパーツを組み合わせて構築
なぜArk UIなのか
// 従来のUIライブラリ(Material-UI、Chakra UI等)
// ❌ デザインが固定される
// ❌ カスタマイズが困難
// ❌ スタイルのオーバーライドが複雑
<Button variant="contained" color="primary">
Click me
</Button>
// Ark UI + 自由なスタイリング
// ✅ 完全に自由なデザイン
// ✅ アクセシビリティは保証
// ✅ 複雑なロジックは任せられる
<Button.Root className={styles.button}>
<Button.Label>Click me</Button.Label>
</Button.Root>
インストール
React
npm install @ark-ui/react
Vue
npm install @ark-ui/vue
Solid
npm install @ark-ui/solid
基本的な使い方(React)
Buttonコンポーネント
import { Button } from '@ark-ui/react';
export const MyButton = () => {
return (
<Button.Root className="my-button">
<Button.Label>Click me</Button.Label>
</Button.Root>
);
};
CSS:
.my-button {
padding: 0.5rem 1rem;
background: #0070f3;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.my-button:hover {
background: #0051cc;
}
.my-button:active {
transform: scale(0.98);
}
Dialogコンポーネント
import { Dialog, Portal } from '@ark-ui/react';
export const MyDialog = () => {
return (
<Dialog.Root>
<Dialog.Trigger className="trigger">Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop className="backdrop" />
<Dialog.Positioner className="positioner">
<Dialog.Content className="content">
<Dialog.Title className="title">Dialog Title</Dialog.Title>
<Dialog.Description className="description">
This is a dialog description.
</Dialog.Description>
<Dialog.CloseTrigger className="close">×</Dialog.CloseTrigger>
<div className="actions">
<button>Cancel</button>
<button>Confirm</button>
</div>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
);
};
CSS:
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.2s;
}
.positioner {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.content {
background: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 90%;
animation: slideUp 0.2s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
主要コンポーネント
Accordion
import { Accordion } from '@ark-ui/react';
export const MyAccordion = () => {
return (
<Accordion.Root defaultValue={['item-1']} multiple>
<Accordion.Item value="item-1">
<Accordion.ItemTrigger>
<h3>Section 1</h3>
<Accordion.ItemIndicator>▼</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<p>Content for section 1</p>
</Accordion.ItemContent>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.ItemTrigger>
<h3>Section 2</h3>
<Accordion.ItemIndicator>▼</Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<p>Content for section 2</p>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
);
};
Select
import { Select, Portal } from '@ark-ui/react';
const items = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
];
export const MySelect = () => {
return (
<Select.Root items={items}>
<Select.Label>Fruits</Select.Label>
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select a fruit" />
<Select.Indicator>▼</Select.Indicator>
</Select.Trigger>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Content>
{items.map((item) => (
<Select.Item key={item.value} item={item}>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
);
};
Tabs
import { Tabs } from '@ark-ui/react';
export const MyTabs = () => {
return (
<Tabs.Root defaultValue="tab-1">
<Tabs.List>
<Tabs.Trigger value="tab-1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab-2">Tab 2</Tabs.Trigger>
<Tabs.Trigger value="tab-3">Tab 3</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="tab-1">
<p>Content for Tab 1</p>
</Tabs.Content>
<Tabs.Content value="tab-2">
<p>Content for Tab 2</p>
</Tabs.Content>
<Tabs.Content value="tab-3">
<p>Content for Tab 3</p>
</Tabs.Content>
</Tabs.Root>
);
};
Menu (Dropdown)
import { Menu, Portal } from '@ark-ui/react';
export const MyMenu = () => {
return (
<Menu.Root>
<Menu.Trigger>Open Menu</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="edit">
<Menu.ItemText>Edit</Menu.ItemText>
<Menu.ItemIndicator>⌘E</Menu.ItemIndicator>
</Menu.Item>
<Menu.Item value="duplicate">
<Menu.ItemText>Duplicate</Menu.ItemText>
<Menu.ItemIndicator>⌘D</Menu.ItemIndicator>
</Menu.Item>
<Menu.Separator />
<Menu.Item value="delete">
<Menu.ItemText>Delete</Menu.ItemText>
<Menu.ItemIndicator>⌫</Menu.ItemIndicator>
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
);
};
Popover
import { Popover, Portal } from '@ark-ui/react';
export const MyPopover = () => {
return (
<Popover.Root>
<Popover.Trigger>Open Popover</Popover.Trigger>
<Portal>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow>
<Popover.ArrowTip />
</Popover.Arrow>
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>This is a popover description.</Popover.Description>
<Popover.CloseTrigger>Close</Popover.CloseTrigger>
</Popover.Content>
</Popover.Positioner>
</Portal>
</Popover.Root>
);
};
Tooltip
import { Tooltip, Portal } from '@ark-ui/react';
export const MyTooltip = () => {
return (
<Tooltip.Root openDelay={300} closeDelay={200}>
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
<Portal>
<Tooltip.Positioner>
<Tooltip.Arrow>
<Tooltip.ArrowTip />
</Tooltip.Arrow>
<Tooltip.Content>This is a tooltip</Tooltip.Content>
</Tooltip.Positioner>
</Portal>
</Tooltip.Root>
);
};
フォームコンポーネント
Checkbox
import { Checkbox } from '@ark-ui/react';
export const MyCheckbox = () => {
return (
<Checkbox.Root>
<Checkbox.Label>Accept terms</Checkbox.Label>
<Checkbox.Control>
<Checkbox.Indicator>✓</Checkbox.Indicator>
</Checkbox.Control>
<Checkbox.HiddenInput />
</Checkbox.Root>
);
};
Radio Group
import { RadioGroup } from '@ark-ui/react';
const options = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];
export const MyRadioGroup = () => {
return (
<RadioGroup.Root>
<RadioGroup.Label>Choose an option</RadioGroup.Label>
{options.map((option) => (
<RadioGroup.Item key={option.value} value={option.value}>
<RadioGroup.ItemText>{option.label}</RadioGroup.ItemText>
<RadioGroup.ItemControl>
<RadioGroup.ItemIndicator />
</RadioGroup.ItemControl>
<RadioGroup.ItemHiddenInput />
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
};
Slider
import { Slider } from '@ark-ui/react';
export const MySlider = () => {
return (
<Slider.Root min={0} max={100} defaultValue={[50]}>
<Slider.Label>Volume</Slider.Label>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0}>
<Slider.HiddenInput />
</Slider.Thumb>
</Slider.Control>
<Slider.ValueText />
</Slider.Root>
);
};
Switch
import { Switch } from '@ark-ui/react';
export const MySwitch = () => {
return (
<Switch.Root>
<Switch.Label>Enable notifications</Switch.Label>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.HiddenInput />
</Switch.Root>
);
};
Panda CSSとの統合
Ark UIはPanda CSSと完璧に統合できます。
インストール
npm install @ark-ui/react @pandacss/dev
npx panda init
Park UIプリセット
Park UIは、Ark UI + Panda CSSのプリセットコンポーネントコレクションです。
npm install @park-ui/react
使用例:
import { Button } from '@park-ui/react';
export const MyButton = () => {
return (
<Button variant="solid" size="lg">
Click me
</Button>
);
};
カスタムスタイリング
import { Dialog, Portal } from '@ark-ui/react';
import { css } from '../styled-system/css';
import { dialog } from '../styled-system/recipes';
export const StyledDialog = () => {
const styles = dialog();
return (
<Dialog.Root>
<Dialog.Trigger className={styles.trigger}>Open</Dialog.Trigger>
<Portal>
<Dialog.Backdrop className={styles.backdrop} />
<Dialog.Positioner className={styles.positioner}>
<Dialog.Content className={styles.content}>
<Dialog.Title className={styles.title}>Title</Dialog.Title>
<Dialog.Description className={styles.description}>
Description
</Dialog.Description>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
);
};
Panda CSS設定:
// panda.config.ts
import { defineConfig } from '@pandacss/dev';
export default defineConfig({
theme: {
extend: {
recipes: {
dialog: {
className: 'dialog',
base: {
backdrop: {
position: 'fixed',
inset: 0,
bg: 'blackAlpha.600',
},
positioner: {
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
content: {
bg: 'white',
p: '6',
borderRadius: 'lg',
boxShadow: 'xl',
maxW: '500px',
},
title: {
fontSize: 'xl',
fontWeight: 'bold',
mb: '2',
},
description: {
color: 'gray.600',
},
},
},
},
},
},
});
Vueでの使用
<script setup lang="ts">
import { Dialog, Portal } from '@ark-ui/vue';
</script>
<template>
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop class="backdrop" />
<Dialog.Positioner class="positioner">
<Dialog.Content class="content">
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>This is a dialog.</Dialog.Description>
<Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.positioner {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
</style>
Solidでの使用
import { Dialog, Portal } from '@ark-ui/solid';
export const MyDialog = () => {
return (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Portal>
<Dialog.Backdrop class="backdrop" />
<Dialog.Positioner class="positioner">
<Dialog.Content class="content">
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>This is a dialog.</Dialog.Description>
<Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
);
};
制御されたコンポーネント
状態を外部管理
import { Dialog, Portal } from '@ark-ui/react';
import { useState } from 'react';
export const ControlledDialog = () => {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<Dialog.Root open={open} onOpenChange={(details) => setOpen(details.open)}>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Controlled Dialog</Dialog.Title>
<Dialog.Description>State is managed externally.</Dialog.Description>
<button onClick={() => setOpen(false)}>Close</button>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</>
);
};
フォームとの統合
import { Select, Portal } from '@ark-ui/react';
import { useForm } from 'react-hook-form';
const fruits = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
];
export const FormWithSelect = () => {
const { register, handleSubmit, setValue } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Select.Root
items={fruits}
onValueChange={(details) => setValue('fruit', details.value[0])}
>
<Select.Label>Choose a fruit</Select.Label>
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select" />
</Select.Trigger>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Content>
{fruits.map((item) => (
<Select.Item key={item.value} item={item}>
<Select.ItemText>{item.label}</Select.ItemText>
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
<input type="hidden" {...register('fruit')} />
</Select.Root>
<button type="submit">Submit</button>
</form>
);
};
アニメーション
Framer Motionとの統合
import { Dialog, Portal } from '@ark-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
export const AnimatedDialog = () => {
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<AnimatePresence>
<Portal>
<Dialog.Backdrop asChild>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
</Dialog.Backdrop>
<Dialog.Positioner>
<Dialog.Content asChild>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
<Dialog.Title>Animated Dialog</Dialog.Title>
<Dialog.Description>With smooth animations</Dialog.Description>
</motion.div>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</AnimatePresence>
</Dialog.Root>
);
};
アクセシビリティ
Ark UIは全コンポーネントがWAI-ARIA準拠で、アクセシビリティが組み込まれています。
キーボード操作
- Dialog: Escape で閉じる、Tab でフォーカス移動
- Menu: 矢印キーで項目移動、Enter で選択
- Select: 矢印キーでオプション選択、Space で開閉
- Tabs: 矢印キーでタブ移動
- Accordion: 矢印キーで項目移動、Space/Enter で開閉
スクリーンリーダー対応
全コンポーネントが適切なARIA属性を持ち、スクリーンリーダーで正しく読み上げられます。
フォーカス管理
Dialog、Popover、Menuなどは自動的にフォーカストラップを実装し、適切なフォーカス管理を行います。
実践例: データテーブル
import { Table, Pagination } from '@ark-ui/react';
import { useState } from 'react';
const data = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'User' },
];
export const DataTable = () => {
const [page, setPage] = useState(1);
const pageSize = 10;
return (
<div>
<Table.Root>
<Table.Head>
<Table.Row>
<Table.Header>Name</Table.Header>
<Table.Header>Email</Table.Header>
<Table.Header>Role</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{data.map((item) => (
<Table.Row key={item.id}>
<Table.Cell>{item.name}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.role}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
<Pagination.Root
count={100}
pageSize={pageSize}
page={page}
onPageChange={(details) => setPage(details.page)}
>
<Pagination.PrevTrigger>Previous</Pagination.PrevTrigger>
<Pagination.Context>
{(api) =>
api.pages.map((page, index) => {
if (page.type === 'page')
return (
<Pagination.Item key={index} {...page}>
{page.value}
</Pagination.Item>
);
return <Pagination.Ellipsis key={index}>...</Pagination.Ellipsis>;
})
}
</Pagination.Context>
<Pagination.NextTrigger>Next</Pagination.NextTrigger>
</Pagination.Root>
</div>
);
};
まとめ
Ark UIは、ヘッドレスUIライブラリの決定版です。
主な利点:
- 完全に自由なデザイン
- React、Vue、Solidで同じAPI
- 完全なアクセシビリティとキーボード操作
- 複雑なロジックが組み込み済み
- Panda CSSと完璧に統合
こんなプロジェクトに最適:
- 独自のデザインシステムを構築したい
- アクセシビリティが重要
- 複数フレームワークで一貫したコンポーネント
- ゼロランタイムCSSを実現したい
Ark UIは、デザインの自由度とアクセシビリティを両立させ、複雑なコンポーネントロジックを抽象化することで、高品質なUIを効率的に構築できる理想的なライブラリです。