はじめに
Svelteでアプリを作るとき、UIコンポーネントの選択肢って意外と限られているんですよね。Reactには山ほどあるのに。
そんな中で見つけたのがBits UI。GitHubで約3,000スター、NPMの月間ダウンロード数は100万を超えている。96人のコントリビューターが開発に参加していて、Svelteコミュニティでは定番になりつつあります。
30代になって思うのは、「スタイルは自分でやりたいけど、アクセシビリティとか状態管理とか面倒な部分は任せたい」というワガママな要求に応えてくれるライブラリは貴重だということ。Bits UIはまさにそれ。
Bits UIとは
Bits UIは、Svelte向けのヘッドレスコンポーネントライブラリです。「ヘッドレス」というのは、スタイルが一切付いていない状態のコンポーネントということ。
公式サイトでは「Flexible, unstyled, and accessible primitives」と説明されています。柔軟で、スタイルなしで、アクセシブルなプリミティブ。まさにその通り。
ReactでいうところのRadix UIやReact Ariaに相当するもので、これらのライブラリから着想を得て設計されています。MITライセンスのオープンソースで、Svelte 5に対応した最新版(v2)がリリースされています。
特徴・メリット
1. 完全なスタイリングの自由度
これが一番のメリット。普通のUIライブラリだと、提供されたデザインをカスタマイズするのに苦労することが多い。
Bits UIは見た目に関する部分が一切ないので、TailwindでもCSSでも、好きな方法でスタイリングできます。デザイナーから「ここ、もうちょっとこうして」と言われても対応しやすいんですよ。
個人的には、この「デザインの制約がない」という点が一番気に入っています。
2. アクセシビリティが最初から入っている
キーボードナビゲーション、フォーカス管理、WAI-ARIA属性。これらを自分で実装すると本当に大変。
Bits UIでは、これらが最初から組み込まれています。Radix UIやReact Spectrumの設計思想を取り入れているので、アクセシビリティのベストプラクティスが反映されている。
これ、意外と重要なんですけど、アクセシビリティ対応は後から入れると工数が跳ね上がる。最初から入っているのは時短になりますね。
3. Svelte 5のRunes対応
最新のSvelte 5で導入されたRunesに対応しています。$stateや$derivedといった新しいリアクティビティシステムと相性が良い。
TypeScriptも79.6%使われていて、型安全性もしっかり確保されています。
4. 軽量で効率的
バンドルサイズは約48.4KB(Minified + Gzipped)。必要なコンポーネントだけをインポートできるので、実際のバンドルサイズはもっと小さくなります。
コスパ的に、この軽さは嬉しい。
5. 豊富なコンポーネント
Accordion、Alert Dialog、Checkbox、Combobox、Dialog、Dropdown Menu、Popover、Select、Slider、Tabs、Tooltipなど、アプリ開発で必要になるコンポーネントが揃っています。
インストール方法
前提条件
- Svelte 5(Runes対応)
- SvelteKitプロジェクト推奨
パッケージのインストール
# npm
npm install bits-ui
# pnpm
pnpm add bits-ui
# yarn
yarn add bits-ui
# bun
bun add bits-ui
これだけ。シンプルですね。
基本的な使い方
Accordionコンポーネント
Bits UIのコンポーネントは、階層構造になっています。Accordionを例に見てみましょう。
<script lang="ts">
import { Accordion } from "bits-ui";
</script>
<Accordion.Root class="w-full max-w-md">
<Accordion.Item value="item-1" class="border-b">
<Accordion.Header>
<Accordion.Trigger class="flex w-full items-center justify-between py-4 font-medium">
よくある質問1
<span class="transition-transform duration-200">▼</span>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="pb-4 text-gray-600">
ここに回答の内容が入ります。アニメーション付きで開閉します。
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2" class="border-b">
<Accordion.Header>
<Accordion.Trigger class="flex w-full items-center justify-between py-4 font-medium">
よくある質問2
<span class="transition-transform duration-200">▼</span>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="pb-4 text-gray-600">
2つ目の回答内容です。
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
Root、Item、Header、Trigger、Contentという構造。直感的で分かりやすいですよね。
Dialogコンポーネント
モーダルダイアログの実装例。
<script lang="ts">
import { Dialog } from "bits-ui";
let open = $state(false);
</script>
<Dialog.Root bind:open>
<Dialog.Trigger
class="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
ダイアログを開く
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50" />
<Dialog.Content
class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-xl"
>
<Dialog.Title class="text-lg font-bold">
確認
</Dialog.Title>
<Dialog.Description class="mt-2 text-gray-600">
この操作を実行してもよろしいですか?
</Dialog.Description>
<div class="mt-4 flex justify-end gap-2">
<Dialog.Close
class="rounded-md border px-4 py-2 hover:bg-gray-100"
>
キャンセル
</Dialog.Close>
<button
class="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
onclick={() => { open = false; }}
>
確認
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Portalを使ってbody直下にレンダリングされるので、z-indexの問題も起きにくい。
Selectコンポーネント
セレクトボックスの実装。
<script lang="ts">
import { Select } from "bits-ui";
const fruits = [
{ value: "apple", label: "りんご" },
{ value: "banana", label: "バナナ" },
{ value: "orange", label: "オレンジ" },
];
let selected = $state<typeof fruits[0] | undefined>(undefined);
</script>
<Select.Root type="single" bind:value={selected}>
<Select.Trigger
class="flex w-48 items-center justify-between rounded-md border px-3 py-2"
>
{selected?.label ?? "選択してください"}
<span>▼</span>
</Select.Trigger>
<Select.Portal>
<Select.Content
class="rounded-md border bg-white shadow-lg"
>
{#each fruits as fruit}
<Select.Item
value={fruit}
class="cursor-pointer px-3 py-2 hover:bg-gray-100 data-[highlighted]:bg-blue-100"
>
{fruit.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Portal>
</Select.Root>
data-[highlighted]のようなデータ属性でスタイリングできるのが便利。
実践的なユースケース
Tailwind CSSとの組み合わせ
Bits UIとTailwind CSSの相性は抜群です。
<script lang="ts">
import { Tabs } from "bits-ui";
</script>
<Tabs.Root value="tab1" class="w-full max-w-lg">
<Tabs.List class="flex border-b">
<Tabs.Trigger
value="tab1"
class="px-4 py-2 text-gray-600 hover:text-gray-900 data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600"
>
概要
</Tabs.Trigger>
<Tabs.Trigger
value="tab2"
class="px-4 py-2 text-gray-600 hover:text-gray-900 data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600"
>
詳細
</Tabs.Trigger>
<Tabs.Trigger
value="tab3"
class="px-4 py-2 text-gray-600 hover:text-gray-900 data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600"
>
設定
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1" class="p-4">
概要の内容がここに入ります。
</Tabs.Content>
<Tabs.Content value="tab2" class="p-4">
詳細の内容がここに入ります。
</Tabs.Content>
<Tabs.Content value="tab3" class="p-4">
設定の内容がここに入ります。
</Tabs.Content>
</Tabs.Root>
data-[state=active]でアクティブ状態のスタイルを指定できる。Tailwindの記法と自然に組み合わせられます。
グローバルCSSでのスタイリング
Tailwindを使わない場合は、データ属性をセレクタにしてグローバルCSSでスタイリングできます。
/* globals.css */
[data-accordion-trigger] {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 1rem 0;
font-weight: 500;
}
[data-accordion-trigger][data-state="open"] > span {
transform: rotate(180deg);
}
[data-accordion-content] {
overflow: hidden;
padding-bottom: 1rem;
color: #4b5563;
}
プロジェクトの方針に合わせて選択できるのが良いですね。
Child Snippetでのカスタマイズ
Svelte 5のSnippet機能を使って、より細かいカスタマイズも可能です。
<script lang="ts">
import { Checkbox } from "bits-ui";
</script>
<Checkbox.Root class="flex items-center gap-2">
{#snippet children({ checked })}
<div
class="h-5 w-5 rounded border-2 flex items-center justify-center
{checked ? 'bg-blue-600 border-blue-600' : 'border-gray-300'}"
>
{#if checked}
<svg class="h-3 w-3 text-white" viewBox="0 0 12 12">
<path
fill="currentColor"
d="M10.28 2.28L4.5 8.06 1.72 5.28a1 1 0 00-1.44 1.44l3.5 3.5a1 1 0 001.44 0l6.5-6.5a1 1 0 00-1.44-1.44z"
/>
</svg>
{/if}
</div>
<span>同意する</span>
{/snippet}
</Checkbox.Root>
状態に応じた表示の切り替えが自然に書けます。
shadcn-svelteとの関係
実は、Svelte版のshadcn(shadcn-svelte)は内部でBits UIを使っています。
「すぐに使える美しいUI」が欲しいならshadcn-svelte、「完全にカスタムなデザインを作りたい」ならBits UI直接、という使い分けですね。
個人的には、デザイナーと協業するプロジェクトではBits UI直接の方が柔軟性があって良いと思います。
まとめ
Bits UIを使って感じた変化:
- スタイリングの自由度: デザインの制約から完全に解放された
- アクセシビリティ: 面倒な実装を任せられるようになった
- 開発効率: 状態管理やキーボード操作の実装時間が削減された
- 型安全性: TypeScriptとの相性が良く、補完が効く
- 学習コスト: 直感的なAPI設計で習得しやすい
30代になって思うのは、「見た目は自分でコントロールしたいけど、裏側の複雑な部分は信頼できるライブラリに任せたい」という要求は正当だということ。Bits UIはまさにそのニーズに応えてくれます。
Svelteでアプリを作っていて「UIコンポーネント、どうしよう」と悩んでいる人には、一度試してみることをおすすめします。ヘッドレスUIの良さが分かると、UIライブラリへの考え方が変わりますよ。