Ark UI + Park UI:ヘッドレスUIコンポーネント完全ガイド
Ark UI + Park UI:ヘッドレスUIコンポーネント完全ガイド
UIコンポーネントライブラリの選択は、プロジェクトの成功を左右する重要な決定です。デザインの柔軟性を保ちつつ、アクセシビリティとユーザビリティを確保する必要があります。
Ark UIは、ヘッドレスUIコンポーネントのライブラリで、ロジックと状態管理を提供しながら、スタイリングの自由を完全に保ちます。Park UIは、Ark UIの上に構築された美しいプリセットコンポーネント集です。
この記事では、Ark UIとPark UIの基本から実践的な使い方まで、詳しく解説していきます。
Ark UIとは
Ark UIは、Chakra UIの開発チームによって作られたヘッドレスUIコンポーネントライブラリです。
主な特徴
- ヘッドレス設計: スタイリングは完全に自由
- フレームワーク対応: React、Vue、Solidをサポート
- アクセシビリティ: ARIA準拠
- TypeScriptファースト: 完全な型安全性
- 状態管理: Zag.js(状態マシン)を使用
- コンポジション: 柔軟なコンポーネント組み合わせ
Park UIとは
Park UIは、Ark UIをベースにした美しいプリセットコンポーネント集です。
- 即座に使える: プリセットされたスタイル
- カスタマイズ可能: Panda CSSベース
- レスポンシブ: モバイルファースト
- ダークモード: 標準でサポート
- アニメーション: スムーズなトランジション
セットアップ
Ark UIのインストール
npm install @ark-ui/react
# または
pnpm add @ark-ui/react
Park UIのインストール
Park UIを使う場合、Panda CSSも必要です。
# Panda CSSのインストール
npm install -D @pandacss/dev
npx panda init
# Park UIコンポーネントのインストール
npx @park-ui/cli init
インストール時の質問:
? Which UI library would you like to use? React
? Which style library would you like to use? Panda CSS
? Where would you like to store your components? components/ui
? Would you like to use TypeScript? Yes
設定ファイル
panda.config.ts が自動生成されます。
import { defineConfig } from "@pandacss/dev";
import { createPreset } from "@park-ui/panda-preset";
export default defineConfig({
preflight: true,
presets: [
"@pandacss/preset-base",
createPreset({
accentColor: "blue",
grayColor: "slate",
borderRadius: "md",
}),
],
include: [
"./src/**/*.{js,jsx,ts,tsx}",
"./pages/**/*.{js,jsx,ts,tsx}",
],
exclude: [],
outdir: "styled-system",
});
基本的な使い方
Ark UIの基本
アコーディオン
// components/accordion-example.tsx
import { Accordion } from "@ark-ui/react";
export function AccordionExample() {
return (
<Accordion.Root>
<Accordion.Item value="item-1">
<Accordion.ItemTrigger>
What is Ark UI?
</Accordion.ItemTrigger>
<Accordion.ItemContent>
Ark UI is a headless UI component library that provides
the logic and state management for building accessible
UI components.
</Accordion.ItemContent>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.ItemTrigger>
Why use headless components?
</Accordion.ItemTrigger>
<Accordion.ItemContent>
Headless components separate logic from presentation,
giving you complete control over styling while ensuring
accessibility and behavior.
</Accordion.ItemContent>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.ItemTrigger>
Is it production ready?
</Accordion.ItemTrigger>
<Accordion.ItemContent>
Yes! Ark UI is used in production by many companies
and is actively maintained.
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
);
}
ダイアログ(モーダル)
// components/dialog-example.tsx
import { Dialog } from "@ark-ui/react";
import { useState } from "react";
export function DialogExample() {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>
This is a dialog description that explains what
this dialog is about.
</Dialog.Description>
<div>
<label htmlFor="name">Name:</label>
<input id="name" type="text" />
</div>
<div>
<Dialog.CloseTrigger>Cancel</Dialog.CloseTrigger>
<button onClick={() => setOpen(false)}>
Save
</button>
</div>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
);
}
Park UIの基本
Park UIを使うと、スタイル済みのコンポーネントをすぐに使えます。
ボタン
npx @park-ui/cli add button
使用例:
// app/page.tsx
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div>
<Button>Default Button</Button>
<Button variant="outline">Outline Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button size="sm">Small Button</Button>
<Button size="lg">Large Button</Button>
</div>
);
}
カード
npx @park-ui/cli add card
使用例:
// components/product-card.tsx
import { Card } from "@/components/ui/card";
export function ProductCard() {
return (
<Card.Root>
<Card.Header>
<Card.Title>Premium Plan</Card.Title>
<Card.Description>
Best for professional use
</Card.Description>
</Card.Header>
<Card.Body>
<div className="text-4xl font-bold">$99</div>
<ul className="space-y-2 mt-4">
<li>✓ Unlimited projects</li>
<li>✓ Priority support</li>
<li>✓ Advanced analytics</li>
</ul>
</Card.Body>
<Card.Footer>
<Button width="full">Subscribe</Button>
</Card.Footer>
</Card.Root>
);
}
主要コンポーネント
セレクト
複雑なドロップダウン選択を簡単に実装できます。
// components/select-example.tsx
import { Select } from "@ark-ui/react";
const frameworks = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "solid", label: "Solid" },
{ value: "svelte", label: "Svelte" },
];
export function SelectExample() {
return (
<Select.Root items={frameworks}>
<Select.Label>Framework</Select.Label>
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select a framework" />
<Select.Indicator>▼</Select.Indicator>
</Select.Trigger>
</Select.Control>
<Select.Positioner>
<Select.Content>
{frameworks.map((framework) => (
<Select.Item key={framework.value} item={framework}>
<Select.ItemText>{framework.label}</Select.ItemText>
<Select.ItemIndicator>✓</Select.ItemIndicator>
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Select.Root>
);
}
コンボボックス(オートコンプリート)
検索可能なセレクトボックスです。
// components/combobox-example.tsx
import { Combobox } from "@ark-ui/react";
import { useState } from "react";
const countries = [
{ value: "jp", label: "Japan" },
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
{ value: "ca", label: "Canada" },
{ value: "au", label: "Australia" },
];
export function ComboboxExample() {
const [items, setItems] = useState(countries);
const handleInputChange = (details: { value: string }) => {
const filtered = countries.filter((country) =>
country.label.toLowerCase().includes(details.value.toLowerCase())
);
setItems(filtered);
};
return (
<Combobox.Root
items={items}
onInputValueChange={handleInputChange}
>
<Combobox.Label>Country</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Search countries..." />
<Combobox.Trigger>▼</Combobox.Trigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
{items.map((country) => (
<Combobox.Item key={country.value} item={country}>
<Combobox.ItemText>{country.label}</Combobox.ItemText>
<Combobox.ItemIndicator>✓</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
);
}
タブ
// components/tabs-example.tsx
import { Tabs } from "@ark-ui/react";
export function TabsExample() {
return (
<Tabs.Root defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="security">Security</Tabs.Trigger>
<Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="account">
<h3>Account Settings</h3>
<p>Manage your account information here.</p>
</Tabs.Content>
<Tabs.Content value="security">
<h3>Security Settings</h3>
<p>Update your password and security preferences.</p>
</Tabs.Content>
<Tabs.Content value="notifications">
<h3>Notification Settings</h3>
<p>Choose how you want to be notified.</p>
</Tabs.Content>
</Tabs.Root>
);
}
トースト(通知)
// components/toast-example.tsx
import { Toast, Toaster, createToaster } from "@ark-ui/react";
const toaster = createToaster({
placement: "top-end",
duration: 3000,
});
export function ToastExample() {
const showToast = () => {
toaster.create({
title: "Success!",
description: "Your changes have been saved.",
type: "success",
});
};
return (
<>
<button onClick={showToast}>Show Toast</button>
<Toaster toaster={toaster}>
{(toast) => (
<Toast.Root key={toast.id}>
<Toast.Title>{toast.title}</Toast.Title>
<Toast.Description>{toast.description}</Toast.Description>
<Toast.CloseTrigger>×</Toast.CloseTrigger>
</Toast.Root>
)}
</Toaster>
</>
);
}
ポップオーバー
// components/popover-example.tsx
import { Popover } from "@ark-ui/react";
export function PopoverExample() {
return (
<Popover.Root>
<Popover.Trigger>Open Settings</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow>
<Popover.ArrowTip />
</Popover.Arrow>
<Popover.Title>Settings</Popover.Title>
<Popover.Description>
Customize your experience
</Popover.Description>
<div>
<label>
<input type="checkbox" />
Enable notifications
</label>
</div>
<div>
<label>
<input type="checkbox" />
Dark mode
</label>
</div>
<Popover.CloseTrigger>Close</Popover.CloseTrigger>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
);
}
フォームコンポーネント
フィールドとバリデーション
// components/form-example.tsx
import { Field } from "@ark-ui/react";
export function FormExample() {
return (
<form>
<Field.Root>
<Field.Label>Email</Field.Label>
<Field.Input
type="email"
name="email"
placeholder="you@example.com"
/>
<Field.HelperText>
We'll never share your email.
</Field.HelperText>
<Field.ErrorText>
Please enter a valid email address.
</Field.ErrorText>
</Field.Root>
<Field.Root>
<Field.Label>Password</Field.Label>
<Field.Input
type="password"
name="password"
placeholder="••••••••"
/>
<Field.HelperText>
Must be at least 8 characters.
</Field.HelperText>
</Field.Root>
</form>
);
}
スライダー
// components/slider-example.tsx
import { Slider } from "@ark-ui/react";
import { useState } from "react";
export function SliderExample() {
const [value, setValue] = useState([50]);
return (
<Slider.Root
min={0}
max={100}
value={value}
onValueChange={(details) => setValue(details.value)}
>
<Slider.Label>Volume: {value[0]}%</Slider.Label>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0}>
<Slider.HiddenInput />
</Slider.Thumb>
</Slider.Control>
</Slider.Root>
);
}
レンジスライダー
// components/range-slider-example.tsx
import { Slider } from "@ark-ui/react";
import { useState } from "react";
export function RangeSliderExample() {
const [value, setValue] = useState([25, 75]);
return (
<Slider.Root
min={0}
max={100}
value={value}
onValueChange={(details) => setValue(details.value)}
>
<Slider.Label>
Price Range: ${value[0]} - ${value[1]}
</Slider.Label>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0}>
<Slider.HiddenInput />
</Slider.Thumb>
<Slider.Thumb index={1}>
<Slider.HiddenInput />
</Slider.Thumb>
</Slider.Control>
</Slider.Root>
);
}
ファイルアップロード
// components/file-upload-example.tsx
import { FileUpload } from "@ark-ui/react";
export function FileUploadExample() {
return (
<FileUpload.Root maxFiles={3} accept="image/*">
<FileUpload.Label>Upload Images</FileUpload.Label>
<FileUpload.Dropzone>
Drag and drop images here or
<FileUpload.Trigger>Browse</FileUpload.Trigger>
</FileUpload.Dropzone>
<FileUpload.ItemGroup>
<FileUpload.Context>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUpload.Item key={file.name} file={file}>
<FileUpload.ItemPreview type="image/*">
<FileUpload.ItemPreviewImage />
</FileUpload.ItemPreview>
<FileUpload.ItemName>{file.name}</FileUpload.ItemName>
<FileUpload.ItemSizeText />
<FileUpload.ItemDeleteTrigger>×</FileUpload.ItemDeleteTrigger>
</FileUpload.Item>
))
}
</FileUpload.Context>
</FileUpload.ItemGroup>
<FileUpload.HiddenInput />
</FileUpload.Root>
);
}
カスタマイズ
テーマのカスタマイズ
Park UIのテーマは panda.config.ts で設定できます。
// panda.config.ts
import { defineConfig } from "@pandacss/dev";
import { createPreset } from "@park-ui/panda-preset";
export default defineConfig({
presets: [
createPreset({
accentColor: "purple", // blue, green, red, etc.
grayColor: "neutral", // slate, gray, zinc, etc.
borderRadius: "lg", // sm, md, lg, xl
}),
],
theme: {
extend: {
tokens: {
colors: {
brand: {
50: { value: "#f5f3ff" },
100: { value: "#ede9fe" },
// ... 他の色
900: { value: "#4c1d95" },
},
},
},
},
},
});
コンポーネントのスタイルカスタマイズ
// components/ui/custom-button.tsx
import { ark } from "@ark-ui/react";
import { styled } from "@/styled-system/jsx";
import { button } from "@/styled-system/recipes";
export const CustomButton = styled(ark.button, button, {
// カスタムスタイル
base: {
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: "wide",
},
});
独自のレシピ作成
// panda.config.ts
export default defineConfig({
theme: {
extend: {
recipes: {
badge: {
className: "badge",
base: {
display: "inline-flex",
alignItems: "center",
borderRadius: "full",
px: 2,
py: 1,
fontSize: "xs",
fontWeight: "semibold",
},
variants: {
variant: {
solid: {
bg: "brand.500",
color: "white",
},
outline: {
border: "1px solid",
borderColor: "brand.500",
color: "brand.500",
},
subtle: {
bg: "brand.100",
color: "brand.700",
},
},
},
defaultVariants: {
variant: "solid",
},
},
},
},
},
});
使用例:
import { styled } from "@/styled-system/jsx";
import { badge } from "@/styled-system/recipes";
const Badge = styled("span", badge);
export function BadgeExample() {
return (
<>
<Badge>Default</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="subtle">Subtle</Badge>
</>
);
}
実践例
データテーブル
// components/data-table.tsx
import { Table } from "@ark-ui/react";
interface User {
id: number;
name: string;
email: string;
role: string;
}
const users: User[] = [
{ id: 1, name: "John Doe", email: "john@example.com", role: "Admin" },
{ id: 2, name: "Jane Smith", email: "jane@example.com", role: "User" },
{ id: 3, name: "Bob Johnson", email: "bob@example.com", role: "User" },
];
export function DataTable() {
return (
<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>
{users.map((user) => (
<Table.Row key={user.id}>
<Table.Cell>{user.name}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
<Table.Cell>{user.role}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
}
ページネーション付きテーブル
// components/paginated-table.tsx
import { Pagination } from "@ark-ui/react";
import { useState } from "react";
export function PaginatedTable() {
const [page, setPage] = useState(1);
const pageSize = 10;
const totalItems = 100;
return (
<>
<Table.Root>{/* テーブル内容 */}</Table.Root>
<Pagination.Root
count={totalItems}
pageSize={pageSize}
page={page}
onPageChange={(details) => setPage(details.page)}
>
<Pagination.PrevTrigger>Previous</Pagination.PrevTrigger>
<Pagination.Context>
{({ pages }) =>
pages.map((page, index) =>
page.type === "page" ? (
<Pagination.Item key={index} {...page}>
{page.value}
</Pagination.Item>
) : (
<Pagination.Ellipsis key={index} index={index}>
...
</Pagination.Ellipsis>
)
)
}
</Pagination.Context>
<Pagination.NextTrigger>Next</Pagination.NextTrigger>
</Pagination.Root>
</>
);
}
ベストプラクティス
1. コンポーネントの再利用
// components/ui/data-display.tsx
import { Card } from "@/components/ui/card";
interface DataDisplayProps {
title: string;
value: string | number;
description?: string;
trend?: "up" | "down";
}
export function DataDisplay({
title,
value,
description,
trend,
}: DataDisplayProps) {
return (
<Card.Root>
<Card.Header>
<Card.Title>{title}</Card.Title>
</Card.Header>
<Card.Body>
<div className="text-3xl font-bold">{value}</div>
{description && (
<div className="text-sm text-gray-600">{description}</div>
)}
{trend && (
<div className={trend === "up" ? "text-green-600" : "text-red-600"}>
{trend === "up" ? "↑" : "↓"}
</div>
)}
</Card.Body>
</Card.Root>
);
}
2. アクセシビリティの確保
Ark UIはデフォルトでアクセシブルですが、さらに改善できます。
<Dialog.Root>
<Dialog.Trigger aria-label="Open settings dialog">
Settings
</Dialog.Trigger>
<Dialog.Content aria-describedby="dialog-description">
<Dialog.Title>Settings</Dialog.Title>
<div id="dialog-description">
Customize your application preferences
</div>
{/* コンテンツ */}
</Dialog.Content>
</Dialog.Root>
3. パフォーマンス最適化
import { lazy, Suspense } from "react";
// 重いコンポーネントは遅延読み込み
const HeavyComponent = lazy(() => import("./HeavyComponent"));
export function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
まとめ
Ark UIとPark UIは、モダンなWebアプリケーションのUI構築に最適なツールです。主な利点は以下の通りです。
- 柔軟性: ヘッドレス設計で完全なスタイル制御
- アクセシビリティ: ARIA準拠で標準対応
- 型安全性: TypeScriptファーストで開発効率向上
- 美しいデザイン: Park UIで即座に使えるコンポーネント
デザインの自由度を保ちつつ、堅牢なロジックとアクセシビリティを得られます。新しいプロジェクトでぜひ試してみてください。