shadcn-vue-inertia

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

shadcn-vue for Inertia Rails

适用于Inertia Rails的shadcn-vue

shadcn-vue patterns adapted for Inertia.js + Rails + Vue 3. NOT Nuxt.
Before using a shadcn-vue example, ask:
  • Does it use Nuxt-specific APIs? (
    useRouter
    ,
    useFetch
    ,
    <NuxtLink>
    ) → Replace with Inertia
    router
    , server props,
    <Link>
  • Does it use
    vee-validate
    +
    zod
    ?
    → Replace with Inertia
    <Form>
    +
    name
    attributes. Inertia handles CSRF, errors, redirects, processing state.
为Inertia.js + Rails + Vue 3适配的shadcn-vue模式。不适用于Nuxt。
在使用shadcn-vue示例前,请确认:
  • 是否使用了Nuxt专属API?
    useRouter
    useFetch
    <NuxtLink>
    )→ 替换为Inertia的
    router
    、服务端props、
    <Link>
  • 是否使用了
    vee-validate
    +
    zod
    → 替换为Inertia的
    <Form>
    +
    name
    属性。Inertia会处理CSRF、错误、重定向、处理状态。

Key Differences from Nuxt Defaults

与Nuxt默认配置的主要差异

shadcn-vue default (Nuxt)Inertia equivalent
useFetch
/
useAsyncData
Server-rendered props via controller
useRouter()
(Nuxt)
router
from
@inertiajs/vue3
<NuxtLink>
<Link>
from
@inertiajs/vue3
vee-validate
+
zod
Inertia
<Form>
component
FormField
,
FormItem
,
FormMessage
Plain
<Input name="...">
+
errors.field
useHead()
(Nuxt)
<Head>
from
@inertiajs/vue3
NEVER use shadcn-vue's
FormField
,
FormItem
,
FormLabel
,
FormMessage
components
— they depend on vee-validate's form context internally and will crash without it. Use plain shadcn-vue
Input
/
Label
/
Select
with
name
attributes inside Inertia
<Form>
, and render errors from the scoped slot's
errors
object.
shadcn-vue默认配置(Nuxt)Inertia等效方案
useFetch
/
useAsyncData
通过控制器获取服务端渲染的props
useRouter()
(Nuxt)
来自
@inertiajs/vue3
router
<NuxtLink>
来自
@inertiajs/vue3
<Link>
vee-validate
+
zod
Inertia的
<Form>
组件
FormField
FormItem
FormMessage
普通
<Input name="...">
+
errors.field
useHead()
(Nuxt)
来自
@inertiajs/vue3
<Head>
请勿使用shadcn-vue的
FormField
FormItem
FormLabel
FormMessage
组件
—— 它们内部依赖vee-validate的表单上下文,没有该依赖会崩溃。 在Inertia
<Form>
中使用带
name
属性的普通shadcn-vue
Input
/
Label
/
Select
组件, 并通过作用域插槽的
errors
对象渲染错误信息。

Setup

配置步骤

npx shadcn-vue@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-vue@latest init
。如果
tsconfig.json
中没有
@/
解析别名,请添加。 请勿在
vite.config.ts
中添加
@/
解析别名
——
vite-plugin-ruby
已提供该配置。

shadcn-vue Inputs in Inertia
<Form>

shadcn-vue输入组件与Inertia
<Form>
的集成

Use plain shadcn-vue
Input
/
Label
/
Button
with
name
attributes inside Inertia
<Form>
. See
inertia-rails-forms
skill (+
references/vue.md
) for full
<Form>
API.
The key pattern: Replace shadcn-vue's
FormField
/
FormItem
/
FormMessage
with plain components + manual error display:
vue
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>

<template>
  <Form method="post" action="/users">
    <template #default="{ errors, processing }">
      <div class="space-y-4">
        <div>
          <Label for="name">Name</Label>
          <Input id="name" name="name" />
          <p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
        </div>

        <div>
          <Label for="email">Email</Label>
          <Input id="email" name="email" type="email" />
          <p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
        </div>

        <Button type="submit" :disabled="processing">
          {{ processing ? 'Creating...' : 'Create User' }}
        </Button>
      </div>
    </template>
  </Form>
</template>
<Select>
requires
name
prop
for Inertia
<Form>
integration:
vue
<template>
  <Select name="role" default-value="member">
    <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
    <SelectContent>
      <SelectItem value="admin">Admin</SelectItem>
      <SelectItem value="member">Member</SelectItem>
    </SelectContent>
  </Select>
</template>
在Inertia
<Form>
中使用带
name
属性的普通shadcn-vue
Input
/
Label
/
Button
组件。 如需完整的
<Form>
API,请参考
inertia-rails-forms
技能(以及
references/vue.md
)。
核心模式: 用普通组件+手动错误显示替换shadcn-vue的
FormField
/
FormItem
/
FormMessage
vue
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>

<template>
  <Form method="post" action="/users">
    <template #default="{ errors, processing }">
      <div class="space-y-4">
        <div>
          <Label for="name">Name</Label>
          <Input id="name" name="name" />
          <p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
        </div>

        <div>
          <Label for="email">Email</Label>
          <Input id="email" name="email" type="email" />
          <p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
        </div>

        <Button type="submit" :disabled="processing">
          {{ processing ? 'Creating...' : 'Create User' }}
        </Button>
      </div>
    </template>
  </Form>
</template>
<Select>
组件需要
name
属性
才能与Inertia
<Form>
集成:
vue
<template>
  <Select name="role" default-value="member">
    <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
    <SelectContent>
      <SelectItem value="admin">Admin</SelectItem>
      <SelectItem value="member">Member</SelectItem>
    </SelectContent>
  </Select>
</template>

Dialog with Inertia Navigation

带Inertia导航的对话框

vue
<script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/vue3'

defineProps<{ open: boolean; user: User }>()
</script>

<template>
  <Dialog
    :open="open"
    @update:open="(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }"
  >
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{{ user.name }}</DialogTitle>
      </DialogHeader>
      <!-- content -->
    </DialogContent>
  </Dialog>
</template>
vue
<script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/vue3'

defineProps<{ open: boolean; user: User }>()
</script>

<template>
  <Dialog
    :open="open"
    @update:open="(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }"
  >
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{{ user.name }}</DialogTitle>
      </DialogHeader>
      <!-- content -->
    </DialogContent>
  </Dialog>
</template>

Table with Server-Side Sorting

服务端排序表格

vue
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { router } from '@inertiajs/vue3'

defineProps<{ users: User[]; sort: string }>()

const handleSort = (column: string) => {
  router.get('/users', { sort: column }, { preserveState: true })
}
</script>

<template>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead class="cursor-pointer" @click="handleSort('name')">
          Name {{ sort === 'name' ? '↑' : '' }}
        </TableHead>
        <TableHead>Email</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow v-for="user in users" :key="user.id">
        <TableCell>{{ user.name }}</TableCell>
        <TableCell>{{ user.email }}</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</template>
Use
<Link>
(not
<a>
) for row links to preserve SPA navigation.
vue
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { router } from '@inertiajs/vue3'

defineProps<{ users: User[]; sort: string }>()

const handleSort = (column: string) => {
  router.get('/users', { sort: column }, { preserveState: true })
}
</script>

<template>
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead class="cursor-pointer" @click="handleSort('name')">
          Name {{ sort === 'name' ? '↑' : '' }}
        </TableHead>
        <TableHead>Email</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow v-for="user in users" :key="user.id">
        <TableCell>{{ user.name }}</TableCell>
        <TableCell>{{ user.email }}</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</template>
行链接请使用
<Link>
(而非
<a>
)以保留SPA导航特性。

Toast with Flash Messages

带闪存消息的提示框

Flash config (
flash_keys
) is in
inertia-rails-controllers
. Flash access (
usePage().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
useFlash
composable and Sonner toast provider. Do NOT load if only reading flash values without toast UI.
闪存配置(
flash_keys
)在
inertia-rails-controllers
中。闪存访问(
usePage().flash
)在
inertia-rails-pages
中。本节仅介绍提示框UI的关联配置
强制要求 — 实现基于闪存的Sonner提示框时,请完整阅读以下文件:
references/flash-toast.md
(约80行)—— 包含完整的
useFlash
组合式函数和Sonner提示框提供器。如果仅读取闪存值而不使用提示框UI,请不要加载该文件

Dark Mode (No Nuxt color-mode)

暗色模式(无需Nuxt color-mode)

npx shadcn-vue@latest init
generates CSS variables for light/dark and
@custom-variant dark (&:is(.dark *));
in your CSS (Tailwind v4). No extra setup needed for the variables themselves.
CRITICAL — prevent flash of wrong theme (FOUC): Add an inline script in
<head>
(before Vue 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
composable (light/dark/system modes, localStorage persistence,
matchMedia
listener) instead of Nuxt color-mode. Toggle via
.dark
class on
<html>
— no provider needed.
执行
npx shadcn-vue@latest init
会为亮色/暗色模式生成CSS变量, 并在你的CSS中添加
@custom-variant dark (&:is(.dark *));
(适用于Tailwind v4)。变量本身无需额外配置。
重要事项 — 避免主题闪烁(FOUC):
<head>
中添加内联脚本(在Vue水合之前):
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
组合式函数(支持亮色/暗色/系统模式、本地存储持久化、
matchMedia
监听器)替代Nuxt color-mode。通过
<html>
标签的
.dark
类切换主题 — 无需提供器。

Vue-Specific Gotchas

Vue专属注意事项

v-model
does NOT work with Inertia
<Form>
<Form>
reads values from input
name
attributes on submit, not from Vue's reactivity system. Using
v-model
creates a second source of truth that
<Form>
ignores:
vue
<!-- BAD — v-model value is ignored by <Form> on submit -->
<Form method="post" action="/users">
  <Input v-model="name" />
</Form>

<!-- GOOD — name attribute is what <Form> reads -->
<Form method="post" action="/users">
  <Input name="name" />
</Form>
Use
v-model
only with
useForm
(where you explicitly manage
form.name
).
usePage()
returns a reactive object — use
computed()
for derived values:
vue
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()

// BAD — not reactive, won't update when page changes:
// const user = page.props.auth.user

// GOOD — reactive, updates on navigation:
const user = computed(() => page.props.auth.user)
</script>
Without
computed()
, destructured values freeze at their initial state and won't update after Inertia navigation.
@update:open
vs
@close
for Dialog
— shadcn-vue Dialog emits
update:open
, not
close
. Using
@close
silently does nothing:
vue
<!-- BAD — @close is not emitted by shadcn-vue Dialog -->
<Dialog @close="handleClose">

<!-- GOOD — @update:open fires on open AND close -->
<Dialog :open="open" @update:open="(isOpen) => { if (!isOpen) handleClose() }">
v-model
不适用于Inertia
<Form>
——
<Form>
在提交时读取输入组件的
name
属性值,而非Vue的响应式系统中的值。使用
v-model
会创建
<Form
忽略的第二数据源:
vue
<!-- 错误用法 — <Form>提交时会忽略v-model的值 -->
<Form method="post" action="/users">
  <Input v-model="name" />
</Form>

<!-- 正确用法 — <Form>读取的是name属性的值 -->
<Form method="post" action="/users">
  <Input name="name" />
</Form>
仅在使用
useForm
(需显式管理
form.name
)时使用
v-model
usePage()
返回响应式对象 —— 请使用
computed()
处理衍生值:
vue
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()

// 错误用法 — 非响应式,页面变化时不会更新:
// const user = page.props.auth.user

// 正确用法 — 响应式,导航时会更新:
const user = computed(() => page.props.auth.user)
</script>
如果不使用
computed()
,解构后的值会冻结在初始状态, Inertia导航后不会更新。
Dialog的
@update:open
@close
—— shadcn-vue Dialog触发
update:open
事件,而非
close
事件。使用
@close
不会有任何效果:
vue
<!-- 错误用法 — shadcn-vue Dialog不会触发@close事件 -->
<Dialog @close="handleClose">

<!-- 正确用法 — @update:open会在打开和关闭时触发 -->
<Dialog :open="open" @update:open="(isOpen) => { if (!isOpen) handleClose() }">

Troubleshooting

故障排查

SymptomCauseFix
FormField
/
FormMessage
crash
Using shadcn-vue form components that depend on vee-validateReplace with plain
Input
/
Label
+
errors.field
display
Select
value not submitted
Missing
name
prop
Add
name="field"
to
<Select>
Dialog closes unexpectedlyMissing or wrong
@update:open
handler
Use
@update:open="(open) => { if (!open) closeHandler() }"
Flash of wrong theme (FOUC)Missing inline
<script>
in
<head>
Add dark mode script before stylesheets
v-model
value not submitted
<Form>
reads
name
attrs, not Vue reactive state
Use
name
attribute; reserve
v-model
for
useForm
only
Shared props stale after navigationDestructured
usePage()
without
computed()
Wrap derived values in
computed(() => ...)
症状原因修复方案
FormField
/
FormMessage
组件崩溃
使用了依赖vee-validate的shadcn-vue表单组件替换为普通
Input
/
Label
+
errors.field
错误显示
Select
组件的值未提交
缺少
name
属性
<Select>
添加
name="field"
对话框意外关闭
@update:open
处理器缺失或错误
使用
@update:open="(open) => { if (!open) closeHandler() }"
主题闪烁(FOUC)
<head>
中缺少内联脚本
在样式表之前添加暗色模式脚本
v-model
的值未提交
<Form>
读取的是name属性值,而非Vue响应式状态
使用name属性;仅在
useForm
中使用v-model
共享props在导航后失效未使用
computed()
解构
usePage()
的值
将衍生值包裹在
computed(() => ...)

Related Skills

相关技能

  • Form component
    inertia-rails-forms
    +
    references/vue.md
    (
    <Form>
    scoped slot, useForm)
  • Flash config
    inertia-rails-controllers
    (flash_keys initializer)
  • Flash access
    inertia-rails-pages
    +
    references/vue.md
    (usePage().flash)
  • URL-driven dialogs
    inertia-rails-pages
    +
    references/vue.md
    (router.get pattern)
  • 表单组件
    inertia-rails-forms
    +
    references/vue.md
    <Form>
    作用域插槽、useForm)
  • 闪存配置
    inertia-rails-controllers
    (flash_keys初始化器)
  • 闪存访问
    inertia-rails-pages
    +
    references/vue.md
    (usePage().flash)
  • URL驱动的对话框
    inertia-rails-pages
    +
    references/vue.md
    (router.get模式)

References

参考资料

Load
references/components.md
(~200 lines) when building shadcn-vue 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-vue组件(如折叠面板、侧边栏、标签页、下拉菜单、 带Inertia模式的确认对话框),请查看
references/components.md
(约200行)。
如果仅使用基础的表单、选择器、对话框或表格,请不要加载
components.md
—— 本文中的示例已足够使用。