はじめに
shadcn/uiを使っていると、「これ、あったらいいのにな」と思うコンポーネントが出てくることがあります。タグ入力とか、カンバンボードとか、カラーピッカーとか。
そんなときに見つけたのがDice UI。shadcn/uiのエコシステムを拡張する形で、かゆいところに手が届くコンポーネントを提供してくれるライブラリです。
正直なところ、最初は「shadcn/uiの派生ライブラリか」程度に思っていたんですよ。でも使ってみると、これがなかなかよくできている。GitHubで1,500スター以上、16人のコントリビューターが開発に参加していて、ちゃんと継続的にメンテナンスされている印象です。
Dice UIとは
Dice UIは、React、TypeScript、Tailwind CSSで構築されたアクセシブルなshadcn/uiコンポーネント集です。shadcn/uiと同じく「コピー&ペースト」形式で導入できるのが特徴。
公式サイトでは「Accessible shadcn/ui components built with React, TypeScript, and Tailwind CSS. Copy-paste ready, and customizable.」と説明されています。
要するに、shadcn/uiには含まれていない高機能コンポーネントを、同じ設計思想で提供するプロジェクトなんですよね。
内部的にはRadix UIをベースにしているので、アクセシビリティもしっかりしています。WCAG準拠、適切なARIA属性、キーボードナビゲーション対応が最初から入っている。MITライセンスのオープンソースです。
特徴・メリット
1. shadcn/uiとの完璧な互換性
これが一番の特徴ですね。shadcn/uiを使っているプロジェクトなら、そのまま導入できる。デザインの一貫性も保たれるし、学習コストもほぼゼロ。
個人的には、「既存のプロジェクトに追加しやすい」というのが大きなメリットだと思います。新しいUIライブラリを入れると、デザインの統一感が崩れたりするんですが、Dice UIはその心配がない。
2. 45以上の実用的なコンポーネント
shadcn/uiにはない、でも実務で欲しくなるコンポーネントが揃っています。
- Tags Input: タグの入力・編集・削除
- Kanban: ドラッグ&ドロップ対応のカンバンボード
- Color Picker: カラー選択UI
- File Upload: ファイルアップロード
- Data Table: 高機能なデータテーブル
- Media Player: メディアプレイヤー
- Sortable: ソート可能なリスト
これ、意外と自分で実装すると大変なコンポーネントばかりなんですよね。特にカンバンとかデータテーブルは、一から作ると数日かかる。それがコピペで使えるのは時短になる。
3. アクセシビリティファースト
すべてのコンポーネントがWCAGガイドラインに準拠しています。キーボードナビゲーション、スクリーンリーダー対応、フォーカス管理が最初から入っている。
30代になって思うのは、アクセシビリティ対応は「後から入れる」と本当に大変だということ。最初から入っているライブラリを選ぶのは、長期的に見て正解だと思います。
4. TypeScriptで型安全
全コンポーネントがTypeScriptで書かれているので、型推論がしっかり効く。propsの補完が効くのは開発効率に直結します。
5. Tailwind CSSでカスタマイズ自由
shadcn/uiと同じく、Tailwind CSSでスタイリングされているので、カスタマイズが簡単。既存のデザインシステムとの統合もスムーズです。
インストール方法
前提条件
- Node.js 18以降
- Reactプロジェクト(Next.js、Vite等)
- Tailwind CSS
- shadcn/uiがセットアップ済み
コンポーネントの追加
shadcn/uiのCLIを使ってインストールできます。
# Tags Inputコンポーネントを追加
npx shadcn@latest add @diceui/tags-input
パッケージマネージャーを直接使う場合:
# npm
npm install @diceui/tags-input
# pnpm
pnpm add @diceui/tags-input
# yarn
yarn add @diceui/tags-input
# bun
bun add @diceui/tags-input
他のコンポーネントも同じパターンで追加できます。
基本的な使い方
Tags Inputコンポーネント
タグの入力・編集・削除ができるコンポーネント。フォームで複数の値を入力させたいときに便利です。
"use client"
import * as React from "react"
import {
TagsInput,
TagsInputLabel,
TagsInputItem,
TagsInputItemText,
TagsInputItemDelete,
TagsInputInput,
TagsInputClear,
} from "@diceui/tags-input"
import { X, RefreshCcw } from "lucide-react"
export function TagsInputDemo() {
const [tags, setTags] = React.useState<string[]>(["React", "TypeScript"])
return (
<TagsInput value={tags} onValueChange={setTags} editable>
<TagsInputLabel className="text-sm font-medium">
スキル
</TagsInputLabel>
<div className="flex flex-wrap items-center gap-1.5 rounded-md border p-2">
{tags.map((tag) => (
<TagsInputItem
key={tag}
value={tag}
className="flex items-center gap-1 rounded bg-secondary px-2 py-1"
>
<TagsInputItemText>{tag}</TagsInputItemText>
<TagsInputItemDelete className="hover:text-destructive">
<X className="h-3.5 w-3.5" />
</TagsInputItemDelete>
</TagsInputItem>
))}
<TagsInputInput
placeholder="スキルを追加..."
className="flex-1 bg-transparent outline-none"
/>
</div>
<TagsInputClear className="mt-2 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
<RefreshCcw className="h-4 w-4" />
クリア
</TagsInputClear>
</TagsInput>
)
}
editableプロップを付けると、タグをダブルクリックして編集できるようになります。これ、地味に便利。
バリデーション付きTags Input
"use client"
import * as React from "react"
import { TagsInput, TagsInputInput, TagsInputItem } from "@diceui/tags-input"
export function ValidatedTagsInput() {
const [tags, setTags] = React.useState<string[]>([])
const [error, setError] = React.useState<string>("")
const handleValidate = (value: string) => {
if (value.length < 2) {
setError("2文字以上で入力してください")
return false
}
if (tags.includes(value)) {
setError("同じタグが既に存在します")
return false
}
setError("")
return true
}
return (
<div>
<TagsInput
value={tags}
onValueChange={setTags}
onValidate={handleValidate}
>
<div className="flex flex-wrap gap-1.5 rounded-md border p-2">
{tags.map((tag) => (
<TagsInputItem key={tag} value={tag}>
{tag}
</TagsInputItem>
))}
<TagsInputInput placeholder="タグを入力..." />
</div>
</TagsInput>
{error && (
<p className="mt-1 text-sm text-destructive">{error}</p>
)}
</div>
)
}
onValidateで入力値を検証できます。フォームバリデーションと組み合わせやすい設計ですね。
ペーストで複数タグを追加
<TagsInput
value={tags}
onValueChange={setTags}
addOnPaste
delimiter=","
>
{/* ... */}
</TagsInput>
addOnPasteを有効にすると、カンマ区切りのテキストをペーストしたときに自動でタグに分割してくれます。CSVからコピペでタグを追加したいときに便利。
実践的なユースケース
ブログ記事のタグ入力フォーム
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
} from "@diceui/tags-input"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { X } from "lucide-react"
const formSchema = z.object({
title: z.string().min(1, "タイトルを入力してください"),
content: z.string().min(10, "本文は10文字以上で入力してください"),
tags: z.array(z.string()).min(1, "タグを1つ以上追加してください"),
})
export function BlogPostForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
content: "",
tags: [],
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>タイトル</FormLabel>
<FormControl>
<Input placeholder="記事タイトル" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>本文</FormLabel>
<FormControl>
<Textarea placeholder="記事の内容..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel>タグ</FormLabel>
<FormControl>
<TagsInput
value={field.value}
onValueChange={field.onChange}
>
<div className="flex flex-wrap gap-1.5 rounded-md border p-2">
{field.value.map((tag) => (
<TagsInputItem
key={tag}
value={tag}
className="flex items-center gap-1 rounded bg-secondary px-2 py-1"
>
{tag}
<TagsInputItemDelete>
<X className="h-3 w-3" />
</TagsInputItemDelete>
</TagsInputItem>
))}
<TagsInputInput
placeholder="タグを追加..."
className="flex-1 bg-transparent outline-none"
/>
</div>
</TagsInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">投稿</Button>
</form>
</Form>
)
}
React Hook FormやZodとの統合もスムーズ。onValueChangeをそのままfield.onChangeに渡せるので、フォームライブラリとの相性が良いですね。
キーボードナビゲーション
Dice UIのコンポーネントは、キーボード操作に完全対応しています。
- Enter: タグを追加
- Backspace: 最後のタグを削除(入力が空のとき)
- Delete: 選択中のタグを削除
- ArrowLeft/Right: タグ間を移動
- Escape: 入力をキャンセル
これ、実装しようとすると結構大変なんですよ。最初から入っているのはありがたい。
shadcn/uiとの使い分け
Dice UIはshadcn/uiを置き換えるものではなく、拡張するもの。使い分けはこんな感じ:
| コンポーネント | shadcn/ui | Dice UI |
|---|---|---|
| Button, Card, Dialog | ○ | - |
| Form, Input, Select | ○ | - |
| Tags Input | - | ○ |
| Kanban | - | ○ |
| Color Picker | - | ○ |
| Data Table(基本) | ○ | - |
| Data Table(高機能) | - | ○ |
基本的なUIはshadcn/ui、高機能なUIはDice UIという棲み分けですね。
まとめ
Dice UIを導入して感じた変化:
- 開発速度: 高機能コンポーネントを一から作らなくて済むようになった
- 一貫性: shadcn/uiとデザインが統一されているので違和感がない
- アクセシビリティ: 対応が最初から入っているので安心
- 学習コスト: shadcn/uiを知っていればほぼゼロ
- 保守性: 活発に開発されているので将来性がある
正直なところ、shadcn/uiを使っているプロジェクトなら、Dice UIは「あったら便利」ではなく「あって当然」のレベルだと思います。
特にTags InputやKanbanなど、自分で実装すると時間がかかるコンポーネントが揃っているのは嬉しい。コスパ的に、これを使わない手はないですね。
まだ試していない人は、公式サイトでコンポーネント一覧を見てみてください。「これも、これもあるのか」と驚くと思いますよ。