关于 shadcn-svelte-inertia
此技能将 shadcn-svelte 和 bits-ui 模式适配到使用 Inertia.js 与 Svelte 的 Rails 应用,而非 SvelteKit。内容涵盖表单、导航、对话框、主题设置及常见集成问题。
主要功能
- 01将 shadcn-svelte 模式适配到 Inertia 与 Rails
- 02说明 Inertia 导航和表单约定
- 03涵盖对话框、表格与 Toast 接入
- 04介绍深色模式和响应式页面状态
- 05提供故障排查及组件参考
适用场景
- 01在 Rails 和 Inertia 应用中构建 shadcn-svelte 界面
- 02将 SvelteKit 示例改写为 Inertia 约定
- 03排查表单提交和页面属性未更新问题
- 04根据 Flash 消息添加 Toast 通知
安装
复制下面的命令,在终端中运行。
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)→ 替换为 Inertiarouter、服务端 props、页面组件 - 是否使用了
sveltekit-superforms+zod? → 替换为 Inertia<Form>+name属性。Inertia 会处理 CSRF、错误、重定向和处理中状态。
与 SvelteKit 默认设置的主要区别
| shadcn-svelte 默认设置(SvelteKit) | Inertia 对应项 |
|---|---|
来自 $app/navigation 的 goto() | 来自 @inertiajs/svelte 的 router |
load 函数 | 通过 Rails 控制器提供服务端渲染的 props |
+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:使用 <Form let:errors let:processing>,而不是 {#snippet}。
<Select> 需要 name prop 才能与 Inertia <Form> 集成:
<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:使用 on:openChange,而不是 onOpenChange。
服务端排序表格
<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>
行链接请使用 <Link> 或 use:inertia(不要使用 <a>),以保留 SPA 导航。
使用 Flash 消息显示 Toast
Flash 配置(flash_keys)位于 inertia-rails-controllers 中。Flash 访问($page.flash)位于 inertia-rails-pages 中。本节仅介绍 Toast UI 的接线方式。
必须——实现基于 flash 的 Sonner Toast 时,请阅读整个文件:
references/flash-toast.md(约 80 行)——完整的 flash 侦听器和 svelte-sonner 集成。如果只读取 flash 值而不使用 Toast UI,请勿加载此文件。
深色模式
npx shadcn-svelte@latest init 会为浅色/深色模式生成 CSS 变量,并在 CSS 中添加 @custom-variant dark (&:is(.dark *));(Tailwind v4)。
重要——避免错误主题闪烁(FOUC): 在 <head> 中添加内联脚本(位于 Svelte hydration 之前):
<%# 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 类切换主题。
使用 <svelte:head>,而不是 <Head>
Svelte 使用原生的 <svelte:head>——Svelte 中没有 Inertia <Head> 组件。
这同样适用于 shadcn 模式(例如,在对话框视图中设置页面标题):
<svelte:head>
<title>{user.name} - Profile</title>
</svelte:head>
Svelte 特有注意事项
bind:value 与 Inertia <Form> 不兼容——<Form> 会在提交时从输入元素的 name 属性读取值,而不是从 Svelte 的响应式绑定中读取。
使用 bind:value 会产生第二个数据源,而 <Form> 会忽略它:
<Form method="post" action="/users">
<Input bind:value={name} />
</Form>
<Form method="post" action="/users">
<Input name="name" />
</Form>
仅在 useForm 中使用 bind:value(此时你需要显式管理 $form.name)。
$page store 更新具有响应性,但解构出的值没有响应性:
<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(响应式语句)。
使用 use:inertia 指令作为 <Link> 的替代方案——对于无法使用 <Link> 的元素(例如表格行、自定义组件),请使用此 action:
<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 过渡 props 与 Inertia 导航——带有 transition* props 的 bits-ui 组件,在 Inertia 页面转换期间,如果退出动画耗时长于导航,可能会显示过时内容。请设置较短的持续时间,或在内容依赖页面 props 时使用 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> 读取的是 name 属性,而不是 Svelte 绑定 | 使用 name 属性;仅在 useForm 中使用 bind:value |
| 导航后共享属性过时 | 解构 $page 时未使用 $derived | 使用 $derived($page.props.auth.user) 实现响应式访问 |
相关技能
- 表单组件 →
inertia-rails-forms+references/svelte.md(<Form>代码片段、useForm) - Flash 配置 →
inertia-rails-controllers(flash_keys 初始化器) - 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 —
上述示例已足够。