はじめに
正直なところ、React版のshadcn/uiを見たとき「いいな、Svelteにもこういうのあればな」と思っていたんですよ。
そしたら、ちゃんとあった。Shadcn-Svelte。
GitHubスター数は約8,000。174人以上のコントリビューターが開発に参加していて、最終コミットも活発。コミュニティ主導のプロジェクトとしては、かなりしっかり運営されている印象です。
30代になって思うのは、UIライブラリ選びで「公式か非公式か」より「メンテナンスされているか」が重要だということ。その点で、Shadcn-Svelteは安心して使えるレベルだと思います。
Shadcn-Svelteとは
Shadcn-Svelteは、React向けのshadcn/uiをSvelteに移植したコミュニティプロジェクトです。公式サイトでは「アクセシブルでカスタマイズ可能なコンポーネントを自由にコピーして利用できます」と説明されています。
React版と同様に、コンポーネントをnpmパッケージとしてインストールするのではなく、ソースコードを自分のプロジェクトにコピーして使う方式。この「コードを所有する」というアプローチが、shadcn系の最大の特徴ですね。
技術スタックはTypeScript 85%、Svelte 8%、CSS 4%という構成。MITライセンスで公開されているので、商用利用も問題なし。
特徴・メリット
1. Svelteらしいシンプルな記法
Svelteを選ぶ理由の一つが「シンプルさ」だと思うんですけど、Shadcn-Svelteもその思想を踏襲しています。
React版だとJSX内でpropsを渡す感じですが、Svelte版はSvelteらしい書き方ができる。コンポーネントがシングルファイルで完結するので、見通しがいいんですよね。
2. コンポーネントが自分のものになる
これはReact版と同じメリット。普通のUIライブラリだと、バージョンアップで破壊的変更が入ったり、カスタマイズに限界があったりする。
Shadcn-Svelteは、コンポーネントのソースコードを自分のプロジェクトにコピーするので、完全にカスタマイズ可能。Tailwindのクラスを直接編集できるのは、QOL上がりますね。
3. Bits UIベースで堅牢
React版がRadix UIを使っているのに対し、Svelte版はBits UIを使っています。アクセシビリティがしっかり確保されているのは同じ。
キーボードナビゲーション、スクリーンリーダー対応など、自分で一から実装すると大変な部分が最初から入っています。これ、意外と重要なんですよ。
4. Tailwind CSSとの完璧な統合
Tailwind CSSユーザーには一択ですね。クラス名でスタイリングできるので、既存のSvelteKit + Tailwindプロジェクトにスムーズに導入できます。
ダークモードの切り替えもCSS変数で管理されているので、テーマカスタマイズも簡単。
5. IDE拡張機能が充実
VSCode拡張機能とJetBrains拡張機能が用意されていて、コンポーネントの追加やドキュメントへのアクセスがIDE内で完結する。この開発体験の良さは、時短になります。
インストール方法
前提条件
- Node.js 18以降
- SvelteKitプロジェクト(Astro、Viteでも可)
- Tailwind CSS
SvelteKitでのセットアップ
まずはTailwind CSS付きのSvelteKitプロジェクトを作成。
pnpm dlx sv create my-app --add tailwindcss
cd my-app
次にShadcn-Svelteの初期化。
pnpm dlx shadcn-svelte@latest init
対話式で以下の設定を聞かれます。
- ベースカラー(Slate、Gray、Zinc、Neutral、Stoneから選択)
- グローバルCSSファイルパス
- libのインポートエイリアス(
$lib推奨) - コンポーネント、ユーティリティ、フック用エイリアス
個人的にはデフォルト設定で問題ないと思います。
コンポーネントの追加
セットアップ完了後、必要なコンポーネントを追加していきます。
# Buttonコンポーネントを追加
pnpm dlx shadcn-svelte@latest add button
# 複数まとめて追加
pnpm dlx shadcn-svelte@latest add button card dialog
追加されたコンポーネントは$lib/components/ui/ディレクトリに配置されます。Svelteは単一ファイル内で複数コンポーネントを定義できないので、1コンポーネントが複数ファイルに分かれていることもあります。index.tsから一括インポートできるので、使う側は気にしなくて大丈夫。
基本的な使い方
Buttonコンポーネント
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<div class="flex gap-4">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
variantは6種類。default、outline、secondary、ghost、destructive、linkが用意されています。
サイズの指定
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button size="sm">Small</Button>
<Button>Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<IconPlus />
</Button>
サイズもsm、default、lgに加えて、icon-sm、icon、icon-lgのアイコンボタン用サイズが用意されています。これ、2025年9月に追加された新機能。
アイコン付きボタン
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import Mail from "lucide-svelte/icons/mail";
</script>
<Button>
<Mail class="mr-2 h-4 w-4" />
Login with Email
</Button>
アイコンはLucide Svelteとの組み合わせが相性いいですね。
ローディング状態
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import Loader2 from "lucide-svelte/icons/loader-2";
</script>
<Button disabled>
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
リンクとしてのボタン
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Button href="/dashboard">Dashboard</Button>
hrefを指定すると自動的にアンカータグとしてレンダリングされます。これ、意外と便利。
実践的なユースケース
Cardコンポーネントでのレイアウト
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Card.Root class="w-[350px]">
<Card.Header>
<Card.Title>プロジェクト作成</Card.Title>
<Card.Description>新しいプロジェクトを作成します</Card.Description>
</Card.Header>
<Card.Content>
<p>プロジェクトの詳細を入力してください。</p>
</Card.Content>
<Card.Footer>
<Button>作成</Button>
</Card.Footer>
</Card.Root>
Svelteの場合、サブコンポーネントは* as Cardでまとめてインポートするのが一般的。直感的ですよね。
Dialogコンポーネント
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog/index.js";
import { Button } from "$lib/components/ui/button/index.js";
</script>
<Dialog.Root>
<Dialog.Trigger asChild let:builder>
<Button variant="outline" builders={[builder]}>ダイアログを開く</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>確認</Dialog.Title>
<Dialog.Description>
この操作を実行してもよろしいですか?
</Dialog.Description>
</Dialog.Header>
<div class="flex justify-end gap-2">
<Button variant="outline">キャンセル</Button>
<Button>確認</Button>
</div>
</Dialog.Content>
</Dialog.Root>
asChildとbuildersパターンは、Svelte版特有の書き方。最初は戸惑うかもしれませんが、慣れると理にかなっていると感じます。
ダークモード対応
Shadcn-Svelteは最初からダークモード対応しています。CSS変数で管理されているので、テーマ切り替えも簡単。
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
let isDark = $state(false);
function toggleTheme() {
isDark = !isDark;
document.documentElement.classList.toggle('dark', isDark);
}
</script>
<Button variant="ghost" size="icon" onclick={toggleTheme}>
{#if isDark}
<Moon class="h-5 w-5" />
{:else}
<Sun class="h-5 w-5" />
{/if}
</Button>
本格的に実装するならmode-watcherライブラリを使うのが定番。システム設定との連携もできます。
React版との違い
正直なところ、React版を使ったことがある人は違和感なく使えると思います。大きな違いは以下の点。
| 項目 | Shadcn-Svelte | shadcn/ui (React) |
|---|---|---|
| ベースUI | Bits UI | Radix UI |
| 記法 | Svelte | JSX |
| 状態管理 | Svelte stores / runes | React hooks |
| サブコンポーネント | * as Component |
直接インポート |
| イベントハンドリング | onclick |
onClick |
コンポーネントの種類や見た目は、ほぼ同等。React版で気に入っていたものは、Svelte版でも同じように使えます。
まとめ
Shadcn-Svelteを導入して感じた変化:
- 開発速度: 美しいUIを素早く作れるようになった
- Svelteらしさ: Svelte特有のシンプルな書き方が活かせる
- 自由度: コンポーネントのカスタマイズに制限がない
- 保守性: コミュニティが活発でメンテナンスされている
- 移行のしやすさ: React版の経験がそのまま活きる
30代になって思うのは、「流行っているから使う」のではなく「自分のプロジェクトに合っているか」で選ぶのが大事だということ。その点で、Shadcn-Svelteは「SvelteKit + Tailwind CSS」の組み合わせには最適解だと思います。
特に「React版のshadcn/uiが良かったけど、今回のプロジェクトはSvelteなんだよな」という人には刺さるはず。コンポーネントの使い勝手はほぼ同じなので、学習コストもほぼゼロ。
まだ試していない人は、公式サイトのコンポーネント一覧を見てみてください。「これ全部Svelteで使えるのか」と驚くと思いますよ。