shadcn-svelte-inertia について
Inertia.js と Svelte を使う Rails アプリ向けに、shadcn-svelte と bits-ui のパターンを適用するスキルです。SvelteKit ではなく、フォーム、ナビゲーション、ダイアログ、テーマ設定や連携時の注意点を扱います。
主な機能
- 01shadcn-svelte のパターンを Inertia と Rails 向けに適用
- 02Inertia のナビゲーションとフォームの規約を解説
- 03ダイアログ、テーブル、トースト連携を網羅
- 04ダークモードとリアクティブなページ状態を解説
- 05トラブルシューティングとコンポーネント資料を提供
活用シーン
- 01Rails と Inertia のアプリで shadcn-svelte の画面を構築
- 02SvelteKit の例を Inertia の規約に合わせて変更
- 03フォーム送信や古いページプロパティの問題を調査
- 04フラッシュメッセージを使ったトースト通知の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ inertia-rails/ skills --skill shadcn-svelte-inertiaインストール後、Claude Code で/shadcn-svelte-inertiaを入力して読み込まれたか確認します。
同梱ファイルreferences/components.mdreferences/flash-toast.md
詳細
Inertia Rails 向け shadcn-svelte
Inertia.js + Rails + Svelte 向けに調整した shadcn-svelte(bits-ui)のパターンです。SvelteKit ではありません。
shadcn-svelte の例を使う前に、次の点を確認してください。
- SvelteKit 固有の API を使っていますか?(
goto、$app/navigation、load関数、+page.svelte)→ Inertia のrouter、サーバープロパティ、ページコンポーネントに置き換えます sveltekit-superforms+zodを使っていますか? → Inertia の<Form>+name属性に置き換えます。Inertia が CSRF、エラー、リダイレクト、処理状態を処理します。
SvelteKit のデフォルトとの主な違い
| shadcn-svelte のデフォルト(SvelteKit) | Inertia での相当機能 |
|---|---|
$app/navigation の goto() | @inertiajs/svelte の router |
load 関数 | Rails コントローラー経由のサーバーレンダリング済みプロパティ |
+page.svelte / +layout.svelte | モジュールスクリプトのレイアウトを含むデフォルトエクスポート |
sveltekit-superforms + zod | Inertia の <Form> コンポーネント |
<svelte:head>(SvelteKit が自動管理) | <svelte:head>(同じです。Svelte には Inertia の <Head> はありません) |
セットアップ
npx shadcn-svelte@latest init を実行します。tsconfig.json に @/ 解決エイリアスがなければ追加します。
vite.config.ts に @/ 解決エイリアスを追加しないでください — vite-plugin-ruby がすでに提供しています。
Inertia の <Form> で使う shadcn-svelte の入力要素
Inertia の <Form> 内では、name 属性を指定した通常の shadcn-svelte の Input/Label/Button を使います。
<Form> API 全体については、inertia-rails-forms スキル(および references/svelte.md)を参照してください。
重要なパターン: {#snippet} を使ってフォームの状態にアクセスします。
<script lang="ts">
import { Form } from '@inertiajs/svelte'
import { Input } from '$lib/components/ui/input'
import { Label } from '$lib/components/ui/label'
import { Button } from '$lib/components/ui/button'
</script>
<Form method="post" action="/users">
{#snippet children({ errors, processing })}
<div class="space-y-4">
<div>
<Label for="name">Name</Label>
<Input id="name" name="name" />
{#if errors.name}<p class="text-sm text-destructive">{errors.name}</p>{/if}
</div>
<div>
<Label for="email">Email</Label>
<Input id="email" name="email" type="email" />
{#if errors.email}<p class="text-sm text-destructive">{errors.email}</p>{/if}
</div>
<Button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create User'}
</Button>
</div>
{/snippet}
</Form>
Svelte 4 では、{#snippet} の代わりに <Form let:errors let:processing> を使います。
Inertia の <Form> と連携するには、<Select> に name プロパティが必要です。
<Select name="role" value="member">
<SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>
Inertia ナビゲーションで使うダイアログ
<script lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '$lib/components/ui/dialog'
import { router } from '@inertiajs/svelte'
let { open, user }: { open: boolean; user: User } = $props()
</script>
<Dialog
{open}
onOpenChange={(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{user.name}</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
Svelte 4 では、onOpenChange の代わりに on:openChange を使います。
サーバーサイドソートを使うテーブル
<script lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '$lib/components/ui/table'
import { router } from '@inertiajs/svelte'
let { users, sort }: { users: User[]; sort: string } = $props()
const handleSort = (column: string) => {
router.get('/users', { sort: column }, { preserveState: true })
}
</script>
<Table>
<TableHeader>
<TableRow>
<TableHead class="cursor-pointer" onclick={() => handleSort('name')}>
Name {sort === 'name' ? '↑' : ''}
</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each users as user (user.id)}
<TableRow>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
SPA ナビゲーションを維持するには、行リンクに <a> ではなく <Link> または use:inertia を使います。
フラッシュメッセージを使うトースト
フラッシュ設定(flash_keys)は inertia-rails-controllers にあります。フラッシュへのアクセス($page.flash)は inertia-rails-pages にあります。このセクションでは、トースト UI の接続のみを説明します。
必須 — Sonner でフラッシュベースのトーストを実装する場合は、ファイル全体を読んでください。
references/flash-toast.md(約80行)— フラッシュ監視と svelte-sonner 連携の全体を説明しています。トースト UI を使わずにフラッシュ値を読むだけの場合は、読み込まないでください。
ダークモード
npx shadcn-svelte@latest init は、ライト/ダーク用の CSS 変数と
CSS(Tailwind v4)の @custom-variant dark (&:is(.dark *)); を生成します。
重要 — テーマのちらつき(FOUC)を防止してください。 <head> 内に、Svelte のハイドレーション前に実行されるインラインスクリプトを追加します。
<%# app/views/layouts/application.html.erb — in <head>, before any stylesheets %>
<script>
document.documentElement.classList.toggle(
"dark",
localStorage.appearance === "dark" ||
(!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
</script>
useAppearance パターン(ライト/ダーク/システムモード、localStorage への永続化、
matchMedia リスナー)を使います。<html> の .dark クラスを切り替えます。
<Head> の代わりに <svelte:head> を使用
Svelte ではネイティブの <svelte:head> を使います。Svelte 向けの Inertia の <Head> コンポーネントはありません。
これは shadcn のパターンにも当てはまります(たとえば、ダイアログビューでページタイトルを設定する場合)。
<svelte:head>
<title>{user.name} - Profile</title>
</svelte:head>
Svelte 固有の注意点
bind:value は Inertia の <Form> では動作しません。 <Form> は Svelte のリアクティブバインディングではなく、送信時に入力要素の name 属性から値を読み取ります。
bind:value を使うと真実の情報源が二重になり、<Form> はその値を無視します。
<Form method="post" action="/users">
<Input bind:value={name} />
</Form>
<Form method="post" action="/users">
<Input name="name" />
</Form>
bind:value は、$form.name を明示的に管理する useForm でのみ使います。
$page ストアの更新はリアクティブですが、分割代入した値はリアクティブではありません。
<script lang="ts">
import { page } from '@inertiajs/svelte'
// BAD — snapshot, won't update after navigation:
// let user = $page.props.auth.user
// GOOD — use $derived for reactive access:
let user = $derived($page.props.auth.user)
</script>
Svelte 4 では、$: user = $page.props.auth.user(リアクティブステートメント)を使います。
<Link> の代替として使う use:inertia ディレクティブ — <Link> にできない要素(テーブル行やカスタムコンポーネントなど)には、このアクションを使います。
<script lang="ts">
import { inertia } from '@inertiajs/svelte'
</script>
<tr use:inertia={{ href: `/users/${user.id}` }} class="cursor-pointer">
<td>{user.name}</td>
</tr>
bits-ui のトランジションプロパティと Inertia ナビゲーション — transition* プロパティを持つ bits-ui コンポーネントは、退出アニメーションがナビゲーションより長く続くと、Inertia のページ遷移中に古いコンテンツを表示する場合があります。期間を短く設定するか、ページプロパティに依存するコンテンツで forceMount を使ってください。
トラブルシューティング
| 症状 | 原因 | 対処方法 |
|---|---|---|
| フォームコンポーネントがクラッシュする | superforms に依存する shadcn-svelte のフォームコンポーネントを使用している | Input/Label と errors.field の表示に置き換えます |
Select の値が送信されない | name プロパティがない | <Select> に name="field" を追加します |
| ダイアログが予期せず閉じる | onOpenChange ハンドラーがない、または誤っている | onOpenChange={(open) => { if (!open) closeHandler() }} を使用します |
| テーマが一瞬ちらつく(FOUC) | <head> にインラインの <script> がない | スタイルシートより前にダークモード用スクリプトを追加します |
bind:value の値が送信されない | <Form> は Svelte のバインディングではなく name 属性を読み取る | name 属性を使い、bind:value は useForm の場合にのみ使用します |
| ナビゲーション後、共有 props が古いままになる | $derived を使わずに $page を分割代入している | リアクティブにアクセスするには $derived($page.props.auth.user) を使います |
関連スキル
- フォームコンポーネント →
inertia-rails-forms+references/svelte.md(<Form>のスニペット、useForm) - Flash の設定 →
inertia-rails-controllers(flash_keys initializer) - Flash へのアクセス →
inertia-rails-pages+references/svelte.md($page.flash) - URL 駆動のダイアログ →
inertia-rails-pages+references/svelte.md(router.get パターン) use:inertiaディレクティブ →inertia-rails-pages+references/svelte.md
参考資料
上記に示されていない shadcn-svelte コンポーネント(Accordion、Sheet、Tabs、DropdownMenu、
Inertia パターンを使った AlertDialog)を構築する場合は、references/components.md(約200行)を読み込みます。
基本的な Form、Select、Dialog、Table の使用方法については、components.md を読み込まないでください。
上記の例で十分です。