For React apps built with shadcn/ui and Tailwind. A complete, tested version of this page is in the repository: examples/react-shadcn. The repository’s bun run test:shadcn checks that these components behave like the built-in ones.
npm i @gistui/react @gistui/styles @gistui/catalogyarn add @gistui/react @gistui/styles @gistui/catalogpnpm add @gistui/react @gistui/styles @gistui/catalogbun add @gistui/react @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 "./index.css"; // @import "tailwindcss"; and the shadcn theme2. Match the theme with tokens
shadcn/ui themes are CSS variables. Point GistUI’s tokens at them and every built-in component follows your theme, light and dark.
import type { GistUITokens } from "@gistui/react";
export const tokens: GistUITokens = {
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. Map components
import { useId } from "react";
import type { ComponentProps } from "@gistui/react";
import { IconSvg, ui, useGistButton, useGistField } from "@gistui/react/ui";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
const str = (v: unknown) => (typeof v === "string" && v !== "" ? v : undefined);
function ShadcnCard({ props, children }: ComponentProps) {
const v = str(props.v) ?? "card";
return (
<Card data-gistui="Card" className={cn("gap-0 py-0", v === "sunk" && "border-0 bg-muted shadow-none", v === "outline" && "shadow-none")}>
<CardContent className="flex flex-col gap-4 p-6">{children}</CardContent>
</Card>
);
}
const VARIANTS = { primary: "default", accent: "default", secondary: "outline", ghost: "ghost", link: "link", danger: "destructive" } as const;
function ShadcnButton(p: ComponentProps) {
const b = useGistButton(p); // do: actions, submit and draft in a Form, opens: a Dialog, links
const variant = VARIANTS[b.variant as keyof typeof VARIANTS] ?? "default";
const inner = <>{b.icon && <IconSvg name={b.icon} />}{!b.iconOnly && b.label}</>;
if (b.href) {
return <Button asChild variant={variant}><a href={b.href} target="_blank" rel="noopener noreferrer">{inner}</a></Button>;
}
return (
<>
<Button variant={variant} size={b.iconOnly ? "icon" : "default"} className={cn(b.full && "w-full")} {...b.buttonProps}>{inner}</Button>
{b.dialog}
</>
);
}
function ShadcnInput(p: ComponentProps) {
const id = useId();
const field = useGistField(p); // validation, error, bind:$var
const value = field.bound
? { value: field.value == null ? "" : String(field.value), onChange: (e: React.ChangeEvent<HTMLInputElement>) => field.setValue(e.target.value) }
: { defaultValue: str(p.props.value) };
return (
<div className="grid gap-2">
{field.label && <Label htmlFor={id}>{field.label}{field.required && <span className="text-destructive">*</span>}</Label>}
<Input id={id} name={field.name} type={str(p.props.type) ?? "text"} placeholder={str(p.props.placeholder)} disabled={field.locked} aria-invalid={field.error ? true : undefined} {...value} />
{field.error && <p role="alert" className="text-sm text-destructive">{field.error}</p>}
</div>
);
}
function ShadcnCheckbox(p: ComponentProps) {
const id = useId();
const field = useGistField(p, "bool");
const value = field.bound
? { checked: field.value === true, onCheckedChange: (c: boolean | "indeterminate") => field.setValue(c === true) }
: { defaultChecked: p.props.checked === true };
return (
<div className="flex items-center gap-2">
{/* With `name`, Radix renders a hidden checkbox input that the Form reads. */}
<Checkbox id={id} name={field.name} disabled={field.locked} {...value} />
<Label htmlFor={id}>{field.label}</Label>
</div>
);
}
export const shadcnUI = ui.extend({ Card: ShadcnCard, Button: ShadcnButton, Input: ShadcnInput, Checkbox: ShadcnCheckbox });The example in the repository also maps TextArea and Select (shadcn’s Select picks one value, so it hands multiple back to GistUI’s own).
4. Render with it
import { GistUI } from "@gistui/react";
import { shadcnUI } from "./gistui-shadcn";
import { tokens } from "./tokens";
<GistUI library={shadcnUI} tokens={tokens} stream={stream} onAction={handle} />;Notes
useGistField(p, "bool")is for one checkbox or switch;"list"for a multi-value control; the default"text"for everything else.- Keep
data-gistui="Card"on your wrapper if you rely on GistUI’s style hooks. - Components you do not map (Chart, Table, DatePicker, Dialog…) stay GistUI’s and take your colours from the tokens.