frontier-python-ts/skills/tailwind/SKILL.md
Tailwind CSS conventions for the Vite + React frontend in this harness. Load whenever styling components, configuring tailwind.config.ts, choosing utility classes, deciding when to extract a component, or working with the design-token system. Pairs with `shadcn-ui` (component library) and `vite-react` (frontend skeleton).
npx skillsauth add jon23d/skillz tailwindInstall 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.
Tailwind is the only styling system for the frontend. There is no Mantine, no styled-components, no CSS Modules, no Emotion. Tailwind utilities + a small set of design tokens (CSS variables) + shadcn/ui components is the entire system.
pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init -p
This generates tailwind.config.ts and postcss.config.js.
tailwind.config.tsUse the TypeScript config — not the JavaScript one. shadcn/ui assumes the file shape below; do not deviate without a reason.
import type { Config } from "tailwindcss"
import animate from "tailwindcss-animate"
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [animate],
} satisfies Config
src/index.cssThe single global stylesheet. It defines the design tokens as CSS variables and pulls in Tailwind's layers.
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... full dark palette ... */
}
* { @apply border-border; }
body { @apply bg-background text-foreground; }
}
These tokens are the same shape shadcn/ui ships. Override the values to match the brand; do not rename the tokens.
// Good — semantic, theme-aware
<div className="bg-card text-card-foreground border border-border">
// Bad — raw, breaks dark mode and brand changes
<div className="bg-white text-slate-900 border border-slate-200">
If you find yourself reaching for slate-500 or gray-700, the design system is missing a token. Add the token, do not hard-code the colour.
Use Tailwind's spacing scale (p-4, gap-6, mt-8). Never p-[13px]. Arbitrary values are an escape hatch for one-offs from a design tool — they should be rare and reviewed.
When you have many utilities on one element, group them in this order. The Prettier prettier-plugin-tailwindcss plugin enforces this automatically — install it.
pnpm add -D prettier-plugin-tailwindcss
// .prettierrc
{ "plugins": ["prettier-plugin-tailwindcss"] }
Three identical groupings of utilities is the threshold. Two is fine — DRY pressure that early just produces premature abstractions.
When you do extract, use the cn() helper from @/lib/utils (provided by shadcn) so consumers can override:
import { cn } from "@/lib/utils"
interface PanelProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "muted"
}
export function Panel({ className, variant = "default", ...props }: PanelProps) {
return (
<div
className={cn(
"rounded-lg border border-border p-6",
variant === "muted" && "bg-muted",
className,
)}
{...props}
/>
)
}
Mobile-first. Use Tailwind's breakpoint prefixes (sm:, md:, lg:, xl:, 2xl:). Never write a media query in CSS.
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
darkMode: ["class"] means dark mode is opt-in by adding dark to a parent (usually <html>). Provide a useTheme() hook that toggles document.documentElement.classList.
Always test the dark palette when you add a new component — the easiest way to ship a regression is to ignore dark mode until later.
focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none on every interactive element. shadcn components have this baked in; preserve it when customising.sr-only for screen-reader-only labels.h-10 min-w-10 (40px) at minimum, h-11 (44px) for primary actions.@apply everywhere in a global stylesheet — defeats the point of utility-first. Use @apply sparingly inside @layer base for things like default button reset.index.css). If you have a Button.css, you are doing it wrong.!important — pass utility classes via className and let cn() merge them.bg-gray-50 instead of bg-muted — semantic tokens, always.prettier-plugin-tailwindcss — class order matters for diff readability and review speed. Keep it on.lg:hidden md:block — wrong order. Mobile-first means smallest first: md:block lg:hidden.vite.config.ts — separate files. Vite reads PostCSS, PostCSS reads Tailwind.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.