Tailwind Component Patterns
Common UI component patterns including cards, navbars, forms, modals, and badges built with Tailwind CSS
You are an expert in building reusable UI components with Tailwind CSS utility classes. ## Key Points - **Use semantic HTML.** A navbar should use `<nav>`, buttons should be `<button>`, and form inputs need associated `<label>` elements with proper `for`/`id` attributes. - **Include focus and hover states.** Every interactive element needs visible `focus:` and `hover:` styles for accessibility and polish. - **Use `aria-label` on icon-only buttons.** A hamburger menu button or close button with no visible text must have an accessible label. - **Keep spacing consistent.** Use a limited set of spacing values within a component (e.g., `p-4` and `gap-3`), not random mixes like `p-3`, `p-5`, `p-7`. - **Extract to framework components.** A `<Card>`, `<Badge>`, or `<Input>` component in React/Vue/Svelte is the correct abstraction, not `@apply`. - **Forgetting `overflow-hidden` on cards with images.** Without it, images extend past `rounded-lg` corners. - **Not using `ring` for focus indicators.** `outline` does not respect `border-radius`. Use `focus:ring-2 focus:ring-indigo-500` for rounded focus indicators. - **Missing `min-w-0` on flex children with text truncation.** `truncate` alone does not work if the flex child can grow unbounded. - **Hardcoding colors instead of using theme tokens.** Components that use raw `bg-blue-600` everywhere are harder to theme than those using `bg-primary`. - **Forgetting dark mode variants on all elements.** A card with `dark:bg-gray-800` still looks wrong if the text inside remains `text-gray-900`.
skilldb get tailwind-skills/Tailwind Component PatternsFull skill: 261 linesCommon Component Patterns — Tailwind CSS
You are an expert in building reusable UI components with Tailwind CSS utility classes.
Overview
Tailwind does not ship pre-built components, but its utility classes make it straightforward to build standard UI patterns. This skill covers battle-tested markup patterns for the most common components: cards, navigation bars, forms, modals, badges, and more.
Core Philosophy
Tailwind components are not a contradiction — they are utility classes organized into meaningful, repeatable structures. The framework deliberately avoids shipping pre-built components because every product has different design requirements, but it gives you the atomic building blocks to assemble any component quickly. The key insight is that your framework's component system (React, Vue, Svelte) is the right abstraction layer for reuse, not CSS class extraction.
When building components with Tailwind, the markup is the source of truth. A card is not a .card class with hidden styles in a stylesheet — it is a visible composition of rounded-lg, border, shadow-sm, and p-6 that any developer can read and understand immediately. This transparency eliminates the "what does this class actually do?" problem that plagues traditional CSS architectures. When a component needs to change, you edit its markup directly rather than tracing through layers of CSS abstractions.
Consistency across components comes from discipline, not from the framework. Stick to the same spacing scale, the same border-radius values, the same color tokens. When every card uses rounded-lg and every input uses rounded-md, the design system enforces itself through convention. The moment you reach for arbitrary values or one-off sizes, you fracture that consistency.
Core Concepts
Components in Tailwind are built by composing utilities directly in markup. For reuse, wrap them in your framework's component system (React, Vue, Svelte, etc.) rather than relying on @apply.
Implementation Patterns
Card
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
<img class="h-48 w-full object-cover" src="cover.jpg" alt="" />
<div class="p-6">
<span class="text-xs font-medium uppercase tracking-wide text-indigo-600">Category</span>
<h3 class="mt-1 text-lg font-semibold text-gray-900">Card Title</h3>
<p class="mt-2 text-sm text-gray-600">
A short description of the card content that provides context.
</p>
<div class="mt-4 flex items-center gap-3">
<img class="h-8 w-8 rounded-full" src="avatar.jpg" alt="" />
<div class="text-sm">
<p class="font-medium text-gray-900">Jane Doe</p>
<p class="text-gray-500">Mar 15, 2026</p>
</div>
</div>
</div>
</div>
Horizontal Card
<div class="flex flex-col overflow-hidden rounded-lg border bg-white shadow-sm sm:flex-row">
<img class="h-48 w-full object-cover sm:h-auto sm:w-48" src="cover.jpg" alt="" />
<div class="flex flex-1 flex-col justify-between p-6">
<div>
<h3 class="text-lg font-semibold text-gray-900">Horizontal Card</h3>
<p class="mt-2 text-sm text-gray-600">Description goes here.</p>
</div>
<a href="#" class="mt-4 text-sm font-medium text-indigo-600 hover:text-indigo-500">
Read more →
</a>
</div>
</div>
Navbar
<header class="border-b border-gray-200 bg-white">
<nav class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<a href="/" class="text-xl font-bold text-gray-900">Brand</a>
<!-- Desktop nav -->
<div class="hidden items-center gap-8 md:flex">
<a href="/features" class="text-sm font-medium text-gray-700 hover:text-gray-900">Features</a>
<a href="/pricing" class="text-sm font-medium text-gray-700 hover:text-gray-900">Pricing</a>
<a href="/docs" class="text-sm font-medium text-gray-700 hover:text-gray-900">Docs</a>
<a href="/login"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Sign in
</a>
</div>
<!-- Mobile menu button -->
<button class="rounded-md p-2 text-gray-500 hover:bg-gray-100 md:hidden" aria-label="Menu">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</nav>
</header>
Form Controls
<form class="space-y-6">
<!-- Text input -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
id="email"
name="email"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm placeholder:text-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
placeholder="you@example.com"
/>
</div>
<!-- Input with error -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
id="password"
name="password"
class="mt-1 block w-full rounded-md border border-red-300 px-3 py-2 text-red-900 shadow-sm placeholder:text-red-300 focus:border-red-500 focus:outline-none focus:ring-1 focus:ring-red-500 sm:text-sm"
aria-describedby="password-error"
/>
<p id="password-error" class="mt-1 text-sm text-red-600">Password must be at least 8 characters.</p>
</div>
<!-- Select -->
<div>
<label for="country" class="block text-sm font-medium text-gray-700">Country</label>
<select
id="country"
name="country"
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
>
<option>United States</option>
<option>Canada</option>
<option>United Kingdom</option>
</select>
</div>
<!-- Checkbox -->
<div class="flex items-center gap-2">
<input
type="checkbox"
id="terms"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<label for="terms" class="text-sm text-gray-700">I agree to the terms</label>
</div>
<!-- Submit -->
<button
type="submit"
class="w-full rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Submit
</button>
</form>
Modal / Dialog
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<!-- Panel -->
<div class="mx-4 w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">Confirm Action</h2>
<button class="rounded-md p-1 text-gray-400 hover:text-gray-600" aria-label="Close">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="mt-3 text-sm text-gray-600">
Are you sure you want to proceed? This action cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-3">
<button class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
Cancel
</button>
<button class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-500">
Delete
</button>
</div>
</div>
</div>
Badges and Pills
<span class="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Active
</span>
<span class="inline-flex items-center rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20">
Inactive
</span>
<span class="inline-flex items-center rounded-full bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">
Pending
</span>
<span class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
Draft
</span>
Avatar Group
<div class="flex -space-x-2">
<img class="h-8 w-8 rounded-full ring-2 ring-white" src="avatar1.jpg" alt="User 1" />
<img class="h-8 w-8 rounded-full ring-2 ring-white" src="avatar2.jpg" alt="User 2" />
<img class="h-8 w-8 rounded-full ring-2 ring-white" src="avatar3.jpg" alt="User 3" />
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-xs font-medium text-gray-600 ring-2 ring-white">
+5
</span>
</div>
Alert / Banner
<div class="rounded-md border border-yellow-200 bg-yellow-50 p-4">
<div class="flex gap-3">
<svg class="h-5 w-5 shrink-0 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.168 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<div>
<h3 class="text-sm font-medium text-yellow-800">Attention needed</h3>
<p class="mt-1 text-sm text-yellow-700">Your trial expires in 3 days. Upgrade now to keep access.</p>
</div>
</div>
</div>
Best Practices
- Use semantic HTML. A navbar should use
<nav>, buttons should be<button>, and form inputs need associated<label>elements with properfor/idattributes. - Include focus and hover states. Every interactive element needs visible
focus:andhover:styles for accessibility and polish. - Use
aria-labelon icon-only buttons. A hamburger menu button or close button with no visible text must have an accessible label. - Keep spacing consistent. Use a limited set of spacing values within a component (e.g.,
p-4andgap-3), not random mixes likep-3,p-5,p-7. - Extract to framework components. A
<Card>,<Badge>, or<Input>component in React/Vue/Svelte is the correct abstraction, not@apply.
Common Pitfalls
- Forgetting
overflow-hiddenon cards with images. Without it, images extend pastrounded-lgcorners. - Not using
ringfor focus indicators.outlinedoes not respectborder-radius. Usefocus:ring-2 focus:ring-indigo-500for rounded focus indicators. - Missing
min-w-0on flex children with text truncation.truncatealone does not work if the flex child can grow unbounded. - Hardcoding colors instead of using theme tokens. Components that use raw
bg-blue-600everywhere are harder to theme than those usingbg-primary. - Forgetting dark mode variants on all elements. A card with
dark:bg-gray-800still looks wrong if the text inside remainstext-gray-900.
Anti-Patterns
-
@applyas a component system. Creating.card,.btn,.badgeclasses with@applyrecreates the very abstraction layer Tailwind was designed to eliminate. Use your framework's component system instead — a<Card>React component is a better abstraction than a.cardCSS class. -
Div soup without semantic HTML. Wrapping everything in
<div>elements and relying solely on visual styling ignores accessibility. Navbars should use<nav>, buttons should use<button>, and lists should use<ul>. Tailwind styles appearance; HTML provides meaning. -
Inconsistent spacing within a component. Mixing
p-3,p-5, andp-7within the same card creates visual noise. Choose a limited set of spacing values per component and stick to them —p-4for content areas andgap-3for element spacing, for example. -
Copy-pasting component markup without extracting. If you find the same 15-line card structure repeated in six places, it is time to extract a framework component. Tailwind's utility-first approach does not mean you should avoid all abstraction — it means the abstraction should live in your component layer, not your CSS.
-
Hardcoding color values instead of using semantic tokens. Components that use
bg-blue-600everywhere are locked to a single visual identity. Use semantic names likebg-primaryvia theme configuration so the entire component set can be re-themed by changing one config value.
Install this skill directly: skilldb add tailwind-skills
Related Skills
Tailwind Animations
Animation utilities, transitions, custom keyframes, and motion patterns with Tailwind CSS
Tailwind Custom Config
Customizing tailwind.config.js to extend themes, add custom colors, fonts, spacing, and configure content paths
Tailwind Dark Mode
Dark mode strategies including class-based toggling, media queries, and CSS variable theming with Tailwind CSS
Tailwind Fundamentals
Utility-first CSS fundamentals with Tailwind including class composition, spacing, typography, and layout primitives
Tailwind Plugins
Writing custom Tailwind CSS plugins to add utilities, components, base styles, and variants
Tailwind Responsive Design
Responsive breakpoints, mobile-first design patterns, and adaptive layouts with Tailwind CSS