frontier-python-ts/skills/shadcn-ui/SKILL.md
shadcn/ui component library guide for the Vite + React frontend in this harness. Load whenever building UI from primitive components, adding a new shadcn component, customising one, or composing forms, modals, tables, and dropdowns. Pairs with `tailwind` (the styling system shadcn is built on) and `vite-react` (the project skeleton).
npx skillsauth add jon23d/skillz shadcn-uiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
shadcn/ui is the only component library in this harness. There is no Mantine, no MUI, no Chakra, no Ant. Every reusable UI primitive (Button, Dialog, DropdownMenu, Form, Select, Toast, Table, Sheet, Tabs, Tooltip…) comes from shadcn/ui — which means it gets copied into your repo, not imported from a package.
shadcn/ui is not an npm package you install once and import from. It is a CLI that copies fully-styled, accessible component source files (built on Radix UI primitives + Tailwind) into your project's src/components/ui/ directory. You then own those files — edit them freely, version them with the rest of the codebase, no upgrade dance.
This is the single most important thing to internalise:
Components are yours. There is no
import { Button } from "shadcn-ui". There isimport { Button } from "@/components/ui/button", wherebutton.tsxlives in your repo.
Run once per project, after Tailwind is set up:
pnpm dlx shadcn@latest init
The CLI asks several questions. Answer them like this:
| Question | Answer |
|---|---|
| Style | default |
| Base color | slate (or whatever the design system specifies) |
| CSS variables | yes — required for theming |
| components.json location | accept default |
| Components alias | @/components |
| Utils alias | @/lib/utils |
| RSC | no (Vite, not Next) |
| TypeScript | yes |
This creates components.json, src/lib/utils.ts (with the cn() helper), and updates tailwind.config.ts and src/index.css.
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add dialog
pnpm dlx shadcn@latest add form
pnpm dlx shadcn@latest add input label select textarea
pnpm dlx shadcn@latest add table
pnpm dlx shadcn@latest add toast
Each add command writes one or more files to src/components/ui/. The CLI also installs the underlying Radix dependency.
After adding, review the generated file. You may want to:
You own these files. Treat them like any other code in the repo — review, refactor, test.
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
export function CreateProjectButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>New project</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create project</DialogTitle>
</DialogHeader>
{/* form goes here */}
</DialogContent>
</Dialog>
)
}
Rules:
@/components/ui/<name>. Never from a third-party package.asChild slots a child as the trigger. Use it instead of nesting buttons inside buttons.Button with the right variant="destructive", do not modify button.tsx for the one-off.react-hook-form + zod + shadcn Formshadcn's Form component wraps react-hook-form. This is the canonical pattern in this harness.
pnpm add react-hook-form @hookform/resolvers zod
pnpm dlx shadcn@latest add form input label
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useCreateProject } from "@/hooks/useCreateProject"
const schema = z.object({
name: z.string().min(1, "Required").max(200),
description: z.string().max(10_000).optional(),
})
type Values = z.infer<typeof schema>
export function CreateProjectForm({ onCreated }: { onCreated: () => void }) {
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: { name: "", description: "" },
})
const { mutateAsync, isPending } = useCreateProject()
const onSubmit = async (values: Values) => {
await mutateAsync(values)
onCreated()
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isPending}>
{isPending ? "Creating…" : "Create"}
</Button>
</form>
</Form>
)
}
Rules:
zod is for form validation only. It is not the runtime validation layer for the API — that is FastAPI/Pydantic. The frontend zod schema is local UI validation.FormMessage shows the validation error automatically — never write {errors.name && <span>...</span>} inline.FormLabel is wired to the Input automatically via htmlFor — never write your own <label>.@tanstack/react-table + shadcn Tableshadcn provides the visual Table primitives; combine them with @tanstack/react-table for sorting, filtering, and pagination.
pnpm add @tanstack/react-table
pnpm dlx shadcn@latest add table
The shadcn docs ship a <DataTable> example you can copy into src/components/ui/data-table.tsx and own.
sonner (shadcn's recommended toast)pnpm dlx shadcn@latest add sonner
Mount <Toaster /> once in RootLayout, then call toast.success("..."), toast.error("...") from anywhere.
import { toast } from "sonner"
await mutation.mutateAsync(values)
toast.success("Project created")
Theme via the CSS variables in src/index.css (set up by shadcn init). Do not edit individual component files to change colours — change the token, the component picks it up.
Dark mode is darkMode: ["class"] (see the tailwind skill). A theme provider that toggles document.documentElement.classList.add("dark") lives in src/components/theme-provider.tsx (a shadcn pattern).
src/
components/
ui/ # shadcn-generated primitives — own them
button.tsx
dialog.tsx
form.tsx
input.tsx
table.tsx
...
layout/ # app-specific layout (RootLayout, Sidebar)
<feature>/ # feature-specific composed components
CreateProjectForm.tsx
ProjectList.tsx
lib/
utils.ts # cn() helper (created by shadcn init)
Use React Testing Library queries by accessible role / label / text — exactly the way shadcn's components are built to be queried. See the tdd skill.
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { CreateProjectForm } from "./CreateProjectForm"
it("submits the form", async () => {
const user = userEvent.setup()
render(<CreateProjectForm onCreated={vi.fn()} />)
await user.type(screen.getByLabelText(/name/i), "My project")
await user.click(screen.getByRole("button", { name: /create/i }))
expect(await screen.findByText(/created/i)).toBeInTheDocument()
})
getByLabelText and getByRole work because shadcn wires FormLabel to its input correctly.
import { Button } from "shadcn-ui" — that package does not exist. Always @/components/ui/button.shadcn add button on a customised component — the CLI overwrites. Customisations are yours; do not regenerate them.<button> instead of using <Button asChild> — you lose focus styles, accessibility, and visual consistency.Dialog. Always.getByTestId — never. shadcn components are built for getByRole / getByLabelText.zod to validate API responses on the frontend — types come from openapi-typescript. Use zod for forms only.development
Use when adding or modifying environment variable handling in TypeScript projects or monorepos — especially when using process.env directly, missing startup validation, sharing env schemas across packages, or encountering "undefined is not a string" errors at runtime from missing env vars.
testing
Use when creating a new skill, editing an existing skill, writing a SKILL.md, or verifying a skill works before deployment.
development
React UI design principles and conventions. Load when building or modifying any user interface or React components. Covers application type detection, visual standards, component design and structure, Mantine (business apps) and Tailwind (consumer apps), accessibility, responsiveness, state management, data fetching, testing, and in-app help patterns.
development
Use when setting up ESLint and/or Prettier in a TypeScript project, adding linting to an existing TypeScript codebase, or configuring typescript-eslint, eslint-config-prettier, or related packages.