frontier-python-ts/skills/ui-design/SKILL.md
React UI design principles and conventions for the Vite + React + Tailwind + shadcn/ui frontend in this harness. Load when building or modifying any user interface or React components. Covers visual standards, component decomposition, accessibility, responsiveness, state management, data fetching via TanStack Query, and in-app help patterns.
npx skillsauth add jon23d/skillz ui-designInstall 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.
The frontend stack is fixed: Vite + React + TypeScript + Tailwind CSS + shadcn/ui (shadcn components are copied into src/components/ui/, built on Radix primitives). There is no Mantine, no Chakra, no Material UI. All visual decisions live within the Tailwind/shadcn ecosystem.
Stack-specific implementation details — installing shadcn components, the Tailwind config, the Vite project layout, the data-fetching architecture — live in the shadcn-ui, tailwind, and vite-react skills respectively. This skill is about design discipline, not about how to install a button.
text-sm, text-base, text-lg, text-xl, ...) — never arbitrary values.p-1 = 4px, p-2 = 8px, p-4 = 16px, ...). No arbitrary values like p-[13px].bg-background, text-foreground, bg-primary, text-primary-foreground, border-border, bg-muted, text-muted-foreground, bg-destructive). Never hardcoded hex values, never raw Tailwind colour utilities like bg-blue-500 for app surfaces.primary token), consistent for all primary buttons.focus-visible:ring defaults are fine — do not strip them).<Button>), secondary outlined (variant="outline"), ghost (variant="ghost"), destructive (variant="destructive"). Min 44×44px touch targets.<Form>/<FormField>/<FormLabel> components — they wire up aria-describedby and validation messages for you).<FormMessage>).react-hook-form + zod resolver. See the shadcn-ui skill for the canonical pattern.<Skeleton>), not full-page spinners. Specific success/error messages.sonner (the shadcn-recommended toast library), not banners or alert boxes for transient feedback.<AlertDialog>.One component, one responsibility. A component file should not exceed 150 lines.
Separate data from presentation. A component that calls useQuery/useMutation should not contain complex JSX — extract data-fetching into a custom hook (or call the hook at the top of a page component) and render focused children.
// Good — hook owns data, page composes focused children
function useUserDashboard() {
const user = useQuery({ queryKey: userKeys.me, queryFn: fetchMe })
const projects = useQuery({ queryKey: projectKeys.all, queryFn: fetchProjects })
return { user, projects }
}
function UserDashboard() {
const { user, projects } = useUserDashboard()
return (
<div className="flex flex-col gap-6">
<UserHeader user={user.data} isLoading={user.isLoading} />
<ProjectList projects={projects.data} isLoading={projects.isLoading} />
</div>
)
}
Extract every visually distinct section as its own component. More than 3 useState calls is a smell. JSX nesting deeper than 3 levels means you missed an extraction.
One component per file. Related files in a folder:
UserCard/
├── index.ts
├── UserCard.tsx
├── UserCard.test.tsx
└── types.ts
Explicit TypeScript interfaces. Required props are necessary, optional have defaults. Prefer callbacks over store references. Never use React.FC. Never hand-write types for API shapes — derive from components["schemas"] (see the openapi-codegen skill).
Prefer composing smaller components over boolean flag props (showHeader, compact, withBorder).
Extract conditionals and transformations into variables before the return statement.
Before reaching for a hand-built primitive, check whether shadcn ships one (Button, Input, Select, Dialog, DropdownMenu, Tabs, Table, Form, Sheet, Tooltip, Popover, Toast/Sonner, AlertDialog, Card, Badge, ...). Copy it into src/components/ui/ via the CLI; do not paste in unverified copies. Customize via cn(...) and Tailwind utilities, not by editing the primitive's structure.
Never use useEffect + useState for data fetching.
apiClient from src/api/client.ts, which is bound to types generated by openapi-typescript from FastAPI's /openapi.json. See the openapi-codegen skill.src/services/*.ts exposes typed functions that throw on error. Components and hooks call services, never the raw client.useQuery for one-shots), not services directly. Hooks use TanStack Query internally.as SomeType casts — the generated types are the contract; if they're wrong, fix the backend, then rerun codegen.useState or useReducerreact-hook-form (the shadcn <Form> wraps it)useState.style props for anything that has a Tailwind equivalent.bg-background, text-foreground, etc.) instead of raw colour utilities.cn() from @/lib/utils (shipped by the shadcn CLI) — never string-concat class names.prettier-plugin-tailwindcss.dark: prefix and the class strategy — already wired by the shadcn init.See the tailwind and shadcn-ui skills for the full setup.
alt text. Decorative: alt="".<FormLabel> — it wires htmlFor automatically). Interactive non-native elements have role and aria-*.<button> (or shadcn <Button>) for actions, <a> (or React Router <Link>) for navigation. Never <div onClick>.aria-pressed. Dialogs: use shadcn <Dialog>/<AlertDialog> — they trap focus, handle Escape, and set role="dialog" for you.Design mobile first. Tailwind's mobile-first breakpoint prefixes (sm:, md:, lg:, xl:, 2xl:) — base styles apply to mobile, prefixes apply at the breakpoint and up. No hardcoded widths for content containers; prefer max-w-* constraints.
Every data-dependent component handles three states: loading, error, success.
<Skeleton> matching the final layout. Not a centered spinner.refetch().shadcn <Tooltip> for one-sentence explanations of controls and icons. Triggers on hover and keyboard focus. 300–500ms delay. Never put critical info only in a tooltip (invisible on touch).
shadcn <Popover> for 2–4 sentence inline explanations of non-obvious form fields. Place after the field label. Keyboard-accessible by default.
Use shadcn's <FormDescription> beneath inputs with non-obvious purpose. Distinct from <FormMessage> (validation errors). Concise.
Icon/illustration, specific heading, 1–2 sentences explaining the entity, primary action button to create the first item. Never generic.
After completing UI changes, take screenshots from e2e tests (not separate scripts). Add page.screenshot() calls into Playwright tests, then remove before committing. Cover: default state, interaction states, validation errors, success, mobile viewport when relevant. Name as route_state-description_viewport.png. See the playwright-e2e skill.
Use React Testing Library + Vitest. Query by accessible role, label, or visible text — never getByTestId. Use userEvent. Test loading, error, success states. Domain objects from test factories built on components["schemas"] types — no inline literals. See the tdd skill.
useEffect + useState for data fetching → use TanStack Queryawait fetch("/api/...") in a component → use the generated apiClient via a serviceas SomeType for an API response → the generated types are the contract; fix the backend insteadinterface User { ... } hand-written for an API shape → use components["schemas"]["User"]bg-blue-500 for app surfaces → use shadcn tokenssrc/components/ui/ to change the structure → wrong layer; wrap it in your own componentdiv/span with onClick → use <button> / shadcn <Button>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.