For Vue and Nuxt apps built with shadcn-vue and Tailwind.
npm i @gistui/vue @gistui/styles @gistui/catalogyarn add @gistui/vue @gistui/styles @gistui/catalogpnpm add @gistui/vue @gistui/styles @gistui/catalogbun add @gistui/vue @gistui/styles @gistui/catalog1. Order the CSS
GistUI’s stylesheet must come before Tailwind, so the cascade layers are theme, base, gistui, components, utilities: above Tailwind’s reset, below its utilities.
import "@gistui/styles/styles.css";
import "./assets/index.css"; // @import "tailwindcss"; and the shadcn-vue theme2. Match the theme with tokens
shadcn-vue uses the same CSS variables as shadcn/ui.
export const tokens = {
font: "inherit",
radius: 10,
bg: "var(--background)",
surface: "var(--card)",
surfaceSunk: "var(--muted)",
surfaceHover: "var(--accent)",
fg: "var(--foreground)",
fgMuted: "var(--muted-foreground)",
border: "var(--border)",
borderStrong: "var(--input)",
primary: "var(--primary)",
primaryFg: "var(--primary-foreground)",
accent: "var(--primary)",
accentFg: "var(--primary-foreground)",
danger: "var(--destructive)",
chart: ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"],
};3. Wrap components
A wrapper receives props (what the model wrote, validated), node and ctx. <GistChildren /> places the children.
<script setup lang="ts">
import { GistChildren, type GistUIComponentProps } from "@gistui/vue";
import { Card, CardContent } from "@/components/ui/card";
defineProps<GistUIComponentProps>();
</script>
<template>
<Card data-gistui="Card">
<CardContent class="flex flex-col gap-4 p-6"><GistChildren /></CardContent>
</Card>
</template><script setup lang="ts">
import { useId } from "vue";
import { useGistField, type GistUIComponentProps } from "@gistui/vue";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const p = defineProps<GistUIComponentProps>();
const id = useId();
const field = useGistField(p); // validation, error, bind:$var
</script>
<template>
<div class="grid gap-2">
<Label v-if="field.label" :for="id">{{ field.label }}</Label>
<Input
:id="id"
:name="field.name"
:type="(p.props.type as string) ?? 'text'"
:placeholder="p.props.placeholder as string"
:disabled="field.locked"
:aria-invalid="field.error ? true : undefined"
:model-value="field.bound ? String(field.value ?? '') : undefined"
:default-value="field.bound ? undefined : (p.props.value as string)"
@update:model-value="(v) => field.bound && field.setValue(v)"
/>
<p v-if="field.error" role="alert" class="text-sm text-destructive">{{ field.error }}</p>
</div>
</template>4. Render with them
<script setup lang="ts">
import { GistUI } from "@gistui/vue";
import GistCard from "./GistCard.vue";
import GistInput from "./GistInput.vue";
import { tokens } from "./tokens";
</script>
<template>
<GistUI :source="answer" :streaming="loading" :components="{ Card: GistCard, Input: GistInput }" :tokens="tokens" @action="onAction" />
</template>Notes
- Keep GistUI’s built-in
Button. With the tokens above it takes your primary colour and radius, and every action, form submit and dialog keeps working. - For a checkbox or switch, call
useGistField(p, "bool"). - Your wrappers see the app’s plugins and whatever the components around
<GistUI>provide. - shadcn-vue is built on Reka UI: for the primitives directly, see Reka UI.