skills/code-architecture-tailwind-v4-best-practices/SKILL.md
Guides Tailwind CSS v4 patterns for buttons and components. Use this skill when creating components with variants, choosing between CVA/tailwind-variants, or configuring Tailwind v4's CSS-first approach.
npx skillsauth add Chris-Maskey/opencode-config code-architecture-tailwind-v4-best-practicesInstall 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.
Use utilities directly in markup as the primary approach. Abstract with CVA/tailwind-variants only when you have 3+ variants.
Tailwind v4's CSS-first configuration eliminates tailwind.config.js entirely. All configuration happens in CSS via @theme directive.
@import "tailwindcss";
@theme {
--color-brand-primary: oklch(0.65 0.24 354.31);
--color-brand-secondary: oklch(0.72 0.11 178);
--font-sans: "Inter", sans-serif;
--radius-button: 0.5rem;
}
Key v4 changes:
@import "tailwindcss" replaces three @tailwind directives--color-* generates color utilities AND exposes as CSS variables.gitignore)// ✅ Simple button - no abstraction needed
<button className="
inline-flex items-center justify-center gap-2
px-4 py-2
bg-blue-500 hover:bg-blue-600 active:bg-blue-700
text-white text-sm font-medium
rounded-md transition-colors
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500
disabled:opacity-50 disabled:pointer-events-none
">
Save Changes
</button>
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
// Base classes
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
outline: "border-2 border-blue-500 text-blue-500 hover:bg-blue-50",
ghost: "text-blue-500 hover:bg-blue-50"
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base"
}
},
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
export type ButtonProps = VariantProps<typeof buttonVariants>;
extendimport { tv, type VariantProps } from 'tailwind-variants';
const card = tv({
slots: {
base: 'rounded-lg border bg-card shadow-sm',
header: 'flex flex-col space-y-1.5 p-6',
title: 'text-2xl font-semibold',
content: 'p-6 pt-0',
footer: 'flex items-center p-6 pt-0'
},
variants: {
variant: {
elevated: { base: 'shadow-xl' },
flat: { base: 'shadow-none border' }
}
}
});
const { base, header, title, content, footer } = card({ variant: 'elevated' });
The Tailwind team discourages @apply except in narrow circumstances. Use component abstractions instead.
/* ❌ Avoid - hides styling decisions, breaks variant support */
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded;
}
/* ✅ Use @utility for custom utilities if absolutely needed */
@utility btn-base {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
| Approach | Bundle | Type Safe | Use Case | |----------|--------|-----------|----------| | Pure Tailwind | 0KB | ❌ | Simple, 1-2 variants, prototyping | | CVA | ~1KB | ✅ | Component libraries, most projects | | Tailwind-variants | ~4KB | ✅ | Complex design systems, slots |
V4 supports native data attributes for clean state management:
export function Button({ isLoading, isDisabled, children }: ButtonProps) {
return (
<button
data-loading={isLoading ?? ""}
data-disabled={isDisabled ?? ""}
className="
bg-blue-500 text-white px-4 py-2 rounded
hover:bg-blue-600
data-loading:opacity-50 data-loading:cursor-wait
data-disabled:opacity-50 data-disabled:pointer-events-none
"
>
{isLoading && <Spinner className="mr-2" />}
{children}
</button>
);
}
Custom variants via @custom-variant:
@custom-variant selected-not-disabled (&[data-selected]:not([data-disabled]));
import { tv, type VariantProps } from 'tailwind-variants';
const buttonStyles = tv({
base: "inline-flex items-center justify-center rounded-md font-medium transition-colors",
variants: {
variant: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300"
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm"
}
}
});
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonStyles>;
export function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
data-slot="button"
className={cn(buttonStyles({ variant, size }), className)}
{...props}
/>
);
}
<button
type="button"
disabled={disabled || loading}
aria-disabled={disabled || loading}
aria-busy={loading}
aria-label={ariaLabel}
className={buttonStyles({ variant, size })}
>
{loading && <Spinner aria-hidden="true" />}
{leftIcon && <span data-slot="icon">{leftIcon}</span>}
<span data-slot="label">{children}</span>
</button>
| v3 | v4 |
|----|-----|
| shadow-sm | shadow-xs |
| rounded-sm | rounded-xs |
| bg-opacity-50 | bg-black/50 |
| bg-gradient-to-r | bg-linear-to-r |
| border (gray-200 default) | border (currentColor) |
| ring (3px blue-500) | ring-3 (currentColor) |
Automated migration: npx @tailwindcss/upgrade
@apply for component stylestools
Anti-patterns and mistakes to avoid as a product manager. Use when evaluating leadership behaviors, improving team dynamics, reflecting on management practices, or onboarding new product managers.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Design effective CTAs using visual attention and gaze psychology principles. Use when designing landing pages, button hierarchies, conversion elements, or optimizing user attention flow through interfaces.
tools
Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include "Vercel Sandbox browser", "microVM Chrome", "agent-browser in sandbox", "browser automation on Vercel", or any task requiring Chrome in a Vercel Sandbox.