shadcn-svelte-inertia

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

shadcn-svelte for Inertia Rails

适用于Inertia Rails的shadcn-svelte

shadcn-svelte (bits-ui) patterns adapted for Inertia.js + Rails + Svelte. NOT SvelteKit.
Before using a shadcn-svelte example, ask:
  • Does it use SvelteKit-specific APIs? (
    goto
    ,
    $app/navigation
    ,
    load
    functions,
    +page.svelte
    ) → Replace with Inertia
    router
    , server props, page components
  • Does it use
    sveltekit-superforms
    +
    zod
    ?
    → Replace with Inertia
    <Form>
    +
    name
    attributes. Inertia handles CSRF, errors, redirects, processing state.
为Inertia.js + Rails + Svelte适配的shadcn-svelte(bits-ui)模式。注意:不适用于SvelteKit
在使用shadcn-svelte示例前,请确认:
  • 是否使用了SvelteKit专属API?
    goto
    $app/navigation
    load
    函数、
    +page.svelte
    )→ 替换为Inertia的
    router
    、服务器props、页面组件
  • 是否使用了
    sveltekit-superforms
    +
    zod
    → 替换为Inertia的
    <Form>
    +
    name
    属性。Inertia会处理CSRF、错误、重定向和处理状态。

Key Differences from SvelteKit Defaults

与SvelteKit默认配置的主要区别

shadcn-svelte default (SvelteKit)Inertia equivalent
goto()
from
$app/navigation
router
from
@inertiajs/svelte
load
functions
Server-rendered props via Rails controller
+page.svelte
/
+layout.svelte
Default exports with module script layout
sveltekit-superforms
+
zod
Inertia
<Form>
component
<svelte:head>
(SvelteKit auto-manages)
<svelte:head>
(same — no Inertia
<Head>
in Svelte)
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>
组件)

Setup

设置步骤

npx shadcn-svelte@latest init
. Add
@/
resolve aliases to
tsconfig.json
if not present. Do NOT add
@/
resolve aliases to
vite.config.ts
vite-plugin-ruby
already provides them.
执行
npx shadcn-svelte@latest init
。如果
tsconfig.json
中没有
@/
解析别名,请添加。 请勿
vite.config.ts
中添加
@/
解析别名——
vite-plugin-ruby
已提供该功能。

shadcn-svelte Inputs in Inertia
<Form>

在Inertia
<Form>
中使用shadcn-svelte输入组件

Use plain shadcn-svelte
Input
/
Label
/
Button
with
name
attributes inside Inertia
<Form>
. See
inertia-rails-forms
skill (+
references/svelte.md
) for full
<Form>
API.
The key pattern: Use
{#snippet}
to access form state:
svelte
<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>
instead of
{#snippet}
.
<Select>
requires
name
prop
for Inertia
<Form>
integration:
svelte
<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
<Form>
内部,使用普通的shadcn-svelte
Input
/
Label
/
Button
并添加
name
属性。 如需完整的
<Form>
API,请参考
inertia-rails-forms
技能(以及
references/svelte.md
)。
核心模式: 使用
{#snippet}
语法访问表单状态:
svelte
<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">姓名</Label>
        <Input id="name" name="name" />
        {#if errors.name}<p class="text-sm text-destructive">{errors.name}</p>{/if}
      </div>

      <div>
        <Label for="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 ? '创建中...' : '创建用户'}
      </Button>
    </div>
  {/snippet}
</Form>
Svelte 4版本:使用
<Form let:errors let:processing>
替代
{#snippet}
<Select>
组件需要
name
属性
才能与Inertia
<Form>
集成:
svelte
<Select name="role" value="member">
  <SelectTrigger><SelectValue placeholder="选择角色" /></SelectTrigger>
  <SelectContent>
    <SelectItem value="admin">管理员</SelectItem>
    <SelectItem value="member">普通成员</SelectItem>
  </SelectContent>
</Select>

Dialog with Inertia Navigation

结合Inertia导航的对话框

svelte
<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>
    <!-- content -->
  </DialogContent>
</Dialog>
Svelte 4:
on:openChange
instead of
onOpenChange
.
svelte
<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

Table with Server-Side Sorting

带服务器端排序的表格

svelte
<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>
Use
<Link>
or
use:inertia
(not
<a>
) for row links to preserve SPA navigation.
svelte
<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')}>
        姓名 {sort === 'name' ? '↑' : ''}
      </TableHead>
      <TableHead>邮箱</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导航特性。

Toast with Flash Messages

结合Flash消息的提示框

Flash config (
flash_keys
) is in
inertia-rails-controllers
. Flash access (
$page.flash
) is in
inertia-rails-pages
. This section covers toast UI wiring only.
MANDATORY — READ ENTIRE FILE when implementing flash-based toasts with Sonner:
references/flash-toast.md
(~80 lines) — full flash watcher and svelte-sonner integration. Do NOT load if only reading flash values without toast UI.
Flash配置(
flash_keys
)在
inertia-rails-controllers
中。Flash消息访问(
$page.flash
)在
inertia-rails-pages
中。本节仅介绍提示框UI的关联方法
强制要求——实现基于Flash的Sonner提示框时,请完整阅读以下文件:
references/flash-toast.md
(约80行)——包含完整的Flash监听和svelte-sonner集成代码。如果仅需读取Flash值而无需提示框UI,请不要加载该文件

Dark Mode

深色模式

npx shadcn-svelte@latest init
generates CSS variables for light/dark and
@custom-variant dark (&:is(.dark *));
in your CSS (Tailwind v4).
CRITICAL — prevent flash of wrong theme (FOUC): Add an inline script in
<head>
(before Svelte hydrates):
erb
<%# 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>
Use a
useAppearance
pattern (light/dark/system modes, localStorage persistence,
matchMedia
listener). Toggle via
.dark
class on
<html>
.
执行
npx shadcn-svelte@latest init
会生成浅色/深色模式的CSS变量,以及CSS中的
@custom-variant dark (&:is(.dark *));
(适用于Tailwind v4)。
关键注意事项——避免主题闪烁(FOUC):
<head>
中添加内联脚本(在Svelte水合之前):
erb
<%# app/views/layouts/application.html.erb — 在<head>中,所有样式表之前 %>
<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>
Instead of
<Head>

使用
<svelte:head>
替代
<Head>

Svelte uses native
<svelte:head>
— there is no Inertia
<Head>
component for Svelte. This applies in shadcn patterns too (e.g., setting page title in dialog views):
svelte
<svelte:head>
  <title>{user.name} - Profile</title>
</svelte:head>
Svelte使用原生的
<svelte:head>
——Svelte中没有Inertia的
<Head>
组件。这同样适用于shadcn的模式(例如在对话框视图中设置页面标题):
svelte
<svelte:head>
  <title>{user.name} - 个人资料</title>
</svelte:head>

Svelte-Specific Gotchas

Svelte专属注意事项

bind:value
does NOT work with Inertia
<Form>
<Form>
reads values from input
name
attributes on submit, not from Svelte's reactive bindings. Using
bind:value
creates a second source of truth that
<Form>
ignores:
svelte
<!-- BAD — bind:value is ignored by <Form> on submit -->
<Form method="post" action="/users">
  <Input bind:value={name} />
</Form>

<!-- GOOD — name attribute is what <Form> reads -->
<Form method="post" action="/users">
  <Input name="name" />
</Form>
Use
bind:value
only with
useForm
(where you explicitly manage
$form.name
).
$page
store updates are reactive, but destructured values are not:
svelte
<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: use
$: user = $page.props.auth.user
(reactive statement).
use:inertia
directive as alternative to
<Link>
— for elements that can't be
<Link>
(e.g., table rows, custom components), use the action:
svelte
<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 transition props and Inertia navigation — bits-ui components with
transition*
props may show stale content during Inertia page transitions if the exit animation outlasts the navigation. Set short durations or use
forceMount
on content that depends on page props.
bind:value
不适用于Inertia
<Form>
——
<Form>
在提交时读取输入的
name
属性值,而非Svelte的响应式绑定值。使用
bind:value
会创建一个
<Form>
会忽略的第二数据源:
svelte
<!-- 错误用法——<Form>提交时会忽略bind:value -->
<Form method="post" action="/users">
  <Input bind:value={name} />
</Form>

<!-- 正确用法——<Form>读取的是name属性值 -->
<Form method="post" action="/users">
  <Input name="name" />
</Form>
仅在使用
useForm
时(需显式管理
$form.name
)才使用
bind:value
$page
store的更新是响应式的,但解构后的值不是:
svelte
<script lang="ts">
  import { page } from '@inertiajs/svelte'

  // 错误用法——仅为快照,导航后不会更新:
  // let user = $page.props.auth.user

  // 正确用法——使用$derived实现响应式访问:
  let user = $derived($page.props.auth.user)
</script>
Svelte 4版本:使用
$: user = $page.props.auth.user
(响应式语句)。
use:inertia
指令作为
<Link>
的替代方案
——对于无法使用
<Link>
的元素(例如表格行、自定义组件),使用该指令:
svelte
<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页面过渡期间,如果退出动画时长超过导航时长,可能会显示陈旧内容。请设置较短的动画时长,或对依赖页面props的内容使用
forceMount

Troubleshooting

故障排除

SymptomCauseFix
Form components crashUsing shadcn-svelte form components that depend on superformsReplace with plain
Input
/
Label
+
errors.field
display
Select
value not submitted
Missing
name
prop
Add
name="field"
to
<Select>
Dialog closes unexpectedlyMissing or wrong
onOpenChange
handler
Use
onOpenChange={(open) => { if (!open) closeHandler() }}
Flash of wrong theme (FOUC)Missing inline
<script>
in
<head>
Add dark mode script before stylesheets
bind:value
not submitted
<Form>
reads
name
attrs, not Svelte bindings
Use
name
attribute; reserve
bind:value
for
useForm
only
Shared props stale after navigationDestructured
$page
without
$derived
Use
$derived($page.props.auth.user)
for reactive access
症状原因解决方法
表单组件崩溃使用了依赖superforms的shadcn-svelte表单组件替换为普通的
Input
/
Label
+
errors.field
错误显示
Select
的值未提交
缺少
name
属性
<Select>
添加
name="field"
对话框意外关闭
onOpenChange
处理器缺失或错误
使用
onOpenChange={(open) => { if (!open) closeHandler() }}
主题闪烁(FOUC)
<head>
中缺少内联脚本
在样式表之前添加深色模式脚本
bind:value
的值未提交
<Form>
读取
name
属性而非Svelte绑定值
使用
name
属性;仅在
useForm
场景下使用
bind:value
共享props在导航后失效解构
$page
时未使用
$derived
使用
$derived($page.props.auth.user)
实现响应式访问

Related Skills

相关技能

  • Form component
    inertia-rails-forms
    +
    references/svelte.md
    (
    <Form>
    snippet, useForm)
  • Flash config
    inertia-rails-controllers
    (flash_keys initializer)
  • Flash access
    inertia-rails-pages
    +
    references/svelte.md
    ($page.flash)
  • URL-driven dialogs
    inertia-rails-pages
    +
    references/svelte.md
    (router.get pattern)
  • use:inertia
    directive
    inertia-rails-pages
    +
    references/svelte.md
  • 表单组件
    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

References

参考资料

Load
references/components.md
(~200 lines) when building shadcn-svelte components beyond those shown above (Accordion, Sheet, Tabs, DropdownMenu, AlertDialog with Inertia patterns).
Do NOT load
components.md
for basic Form, Select, Dialog, or Table usage — the examples above are sufficient.
当构建上述示例之外的shadcn-svelte组件(如Accordion、Sheet、Tabs、DropdownMenu、结合Inertia模式的AlertDialog)时,请加载
references/components.md
(约200行)。
如果仅使用基础的Form、Select、Dialog或Table,请不要加载
components.md
——上述示例已足够。