skills/css-tailwind-styling/SKILL.md
Expert guidance for writing clean, performant CSS and Tailwind CSS. Use when creating styles, designing components, optimizing performance, or establishing styling conventions. Covers modern CSS features, Tailwind utility patterns, responsive design, accessibility, and team collaboration standards.
npx skillsauth add adonis0123/adonis-skills css-tailwind-stylingInstall 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.
This skill provides comprehensive best practices for writing maintainable, performant, and accessible styles using both traditional CSS and Tailwind CSS. It covers modern techniques, performance optimization, responsive design patterns, and team collaboration standards.
Use this protocol before giving Tailwind-specific guidance.
Inspect package.json first:
tailwindcss major version 4 -> initial state: v4tailwindcss major version 3 -> initial state: v3unknown (continue with signal checks)Use these signals to validate or refine the initial state:
@import "tailwindcss", @theme, @utility, @custom-variant@tailwind base;, @tailwind components;, @tailwind utilities; with a config-driven setupv4: dependency and signals align to v4v3: dependency and signals align to v3conflict: dependency and syntax/config signals disagreeunknown: insufficient evidence to identify a single versionv4: Provide v4 as Primary Path, include v3 as Alternate Pathv3: Provide v3 as Primary Path, include v4 as Alternate Pathconflict: Do not stop; provide both repair paths and a recommended path in Conflict Fixunknown: Provide both paths, default recommendation is v4 for new projectsPrefer Tailwind when at least one of these is true:
Prefer traditional CSS (or CSS Modules) when the UI scope is small, mostly static, or the team is not ready to maintain a shared utility vocabulary yet.
Use the Concentric CSS ordering method for utility classes:
// ✅ GOOD: Ordered classes
<div className="
relative z-10 // 1. Positioning
flex items-center // 2. Display & Box Model
w-full max-w-screen-lg mx-auto px-4 // 3. Sizing & Spacing
border border-gray-200 rounded-lg // 4. Borders
bg-white shadow-md // 5. Backgrounds
text-lg font-semibold text-gray-900 // 6. Typography
transition-all duration-200 // 7. Other
">
// ❌ BAD: Random ordering
<div className="text-lg bg-white flex border w-full shadow-md px-4">
Automation: Always recommend the Prettier Plugin for Tailwind CSS:
npm install -D prettier prettier-plugin-tailwindcss
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"]
}
// ❌ BAD: Redundant classes
<div className="ml-2 mr-2 pt-4 pb-4 pl-4 pr-4">
// ✅ GOOD: Use shorthand
<div className="mx-2 py-4 px-4">
// ✅ BETTER: Combine where possible
<div className="mx-2 p-4">
// ❌ BAD: Unnecessary default values
<div className="block lg:flex flex-row justify-center">
// ✅ GOOD: Omit defaults (flex-row is default)
<div className="block lg:flex justify-center">
Use mobile-first defaults and add prefixes only for breakpoint-specific overrides:
// ❌ BAD: Duplicates intent and makes defaults harder to read
<div className="flex flex-col justify-center lg:flex lg:flex-col lg:justify-center">
// ✅ GOOD: Base styles stay unprefixed, only overrides use breakpoints
<div className="flex flex-col justify-center lg:flex-row lg:justify-between">
Priority: Components > @apply
// ❌ BAD: Overuse of @apply
.btn-primary {
@apply bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700;
}
// ✅ GOOD: Create reusable component
function Button({ children, variant = 'primary' }) {
const variants = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-600 hover:bg-gray-700 text-white',
danger: 'bg-red-600 hover:bg-red-700 text-white',
};
return (
<button className={`px-4 py-2 rounded-lg font-medium transition-colors ${variants[variant]}`}>
{children}
</button>
);
}
When to use @apply:
.btn-blue { @apply btn; })Use version-appropriate configuration patterns and keep design tokens centralized.
v3 (config-first):
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html',
],
theme: {
extend: {
colors: {
brand: {
primary: '#1DA1F2',
secondary: '#14171A',
accent: '#1DA1F2',
},
},
spacing: {
'128': '32rem',
'144': '36rem',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
},
},
plugins: [],
};
v4 (CSS-first):
/* app.css */
@import "tailwindcss";
@theme {
--color-brand-primary: #1DA1F2;
--color-brand-secondary: #14171A;
--font-sans: Inter, system-ui, sans-serif;
}
/* Add extra scan paths only when auto-detection is insufficient */
@source "../packages/ui/src/**/*.{ts,tsx}";
Benefits for both versions:
NEVER use string interpolation for class names:
// ❌ VERY BAD: Tailwind cannot detect these
<div className={`text-${color}-500`}>
<div className={`bg-${theme}-100 text-${theme}-900`}>
// ✅ GOOD: Complete class names
<div className={color === 'blue' ? 'text-blue-500' : 'text-red-500'}>
// ✅ BETTER: Object mapping
const colorClasses = {
blue: 'text-blue-500 bg-blue-50',
red: 'text-red-500 bg-red-50',
green: 'text-green-500 bg-green-50',
};
<div className={colorClasses[color]}>
// ✅ BEST: Use a library like clsx or classnames
import clsx from 'clsx';
<div className={clsx(
'px-4 py-2 rounded',
isActive && 'bg-blue-500 text-white',
isDisabled && 'opacity-50 cursor-not-allowed'
)}>
Define component variants explicitly rather than accepting arbitrary classes:
// ❌ BAD: Arbitrary classes via props (override conflicts)
<Button className="bg-red-500" />
// ✅ GOOD: Predefined variants
const Button = ({ variant = 'primary', size = 'md', children }) => {
const variants = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-600 hover:bg-gray-700 text-white',
danger: 'bg-red-600 hover:bg-red-700 text-white',
ghost: 'bg-transparent hover:bg-gray-100 text-gray-700',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button className={`
rounded-lg font-medium transition-colors
${variants[variant]}
${sizes[size]}
`}>
{children}
</button>
);
};
Tailwind doesn't handle accessibility automatically. You must:
// ✅ Proper semantic HTML
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
aria-label="Submit form"
type="submit"
>
Submit
</button>
// ✅ Color contrast (WCAG AA: 4.5:1 minimum)
// Use tools like https://webaim.org/resources/contrastchecker/
<div className="bg-gray-900 text-white"> // High contrast ✓
<div className="bg-gray-300 text-gray-400"> // Poor contrast ✗
// ✅ Focus states
<button className="
bg-blue-500 hover:bg-blue-600
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
">
// ✅ Screen reader only content
<span className="sr-only">Skip to main content</span>
// v3: ensure correct content paths (JIT relies on these)
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
// Add all paths where Tailwind classes exist in v3 projects
],
};
// v4: automatic source detection by default.
// Use @source when classes are generated outside default scan roots.
// In all cases, keep NODE_ENV=production for production builds.
# Install ESLint plugin
npm install -D eslint-plugin-tailwindcss
// .eslintrc.json
{
"extends": ["plugin:tailwindcss/recommended"],
"rules": {
"tailwindcss/classnames-order": "warn",
"tailwindcss/no-custom-classname": "warn",
"tailwindcss/no-contradicting-classname": "error"
}
}
Class Soup Problem:
// ❌ BAD: Unreadable
<div className="px-4 py-2 bg-blue-500 text-white rounded-md shadow-md hover:bg-blue-600 transition duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50">
// ✅ FIX: Extract to component
<PrimaryButton />
Missing Accessibility:
// ❌ BAD
<div className="cursor-pointer" onClick={handleClick}>Click</div>
// ✅ GOOD
<button className="cursor-pointer" onClick={handleClick}>Click</button>
Bundle Bloat:
// ❌ BAD: Empty or wrong content paths
module.exports = {
content: [], // Nothing gets scanned!
}
// ✅ GOOD
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
}
styles/
├── base/
│ ├── reset.css # CSS reset or normalize
│ ├── typography.css # Font styles
│ └── variables.css # CSS custom properties
├── components/
│ ├── buttons.css
│ ├── cards.css
│ └── forms.css
├── layouts/
│ ├── grid.css
│ ├── header.css
│ └── footer.css
├── utilities/
│ └── helpers.css # Utility classes
└── main.css # Main entry point
BEM (Block Element Modifier) - Recommended:
/* Block */
.card {}
/* Element */
.card__header {}
.card__body {}
.card__footer {}
/* Modifier */
.card--featured {}
.card--compact {}
.card__header--large {}
SMACSS Alternative:
/* Base */
body, h1, p {}
/* Layout */
.l-header {}
.l-sidebar {}
.l-main {}
/* Module/Component */
.card {}
.button {}
/* State */
.is-active {}
.is-hidden {}
.is-loading {}
/* Theme */
.theme-dark {}
.theme-light {}
.element {
/* Positioning */
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
/* Display & Box Model */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
margin: 0 auto;
padding: 20px;
/* Borders */
border: 1px solid #ddd;
border-radius: 8px;
/* Backgrounds */
background-color: #fff;
background-image: url('...');
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Typography */
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 1.5;
color: #333;
text-align: center;
/* Other */
opacity: 1;
cursor: pointer;
transition: all 0.3s ease;
}
:root {
/* Colors */
--color-primary: #1DA1F2;
--color-secondary: #14171A;
--color-accent: #F91880;
--color-background: #FFFFFF;
--color-text: #0F1419;
--color-text-secondary: #536471;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Typography */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'Fira Code', monospace;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* Borders */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #000000;
--color-text: #E7E9EA;
--color-text-secondary: #71767B;
}
}
/* Usage */
.button {
background-color: var(--color-primary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
font-family: var(--font-sans);
box-shadow: var(--shadow-md);
}
/* ❌ BAD: Overly specific */
header nav ul li a.active {}
/* ✅ GOOD: Low specificity */
.nav-link.is-active {}
/* ❌ BAD: Nested too deep */
article.main .content .sidebar p.intro {}
/* ✅ GOOD: Flat and specific */
.sidebar-intro {}
/* ❌ BAD: Element + class (unnecessarily specific) */
div.card {}
p.description {}
/* ✅ GOOD: Class only */
.card {}
.description {}
Specificity Tips:
!important (except for utility classes)Flexbox:
.flex-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.flex-item {
flex: 1 1 300px; /* grow shrink basis */
}
CSS Grid:
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
/* Named grid areas */
.layout {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
grid-template-columns: 250px 1fr 1fr;
gap: 20px;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Container Queries (2024):
.container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 500px) {
.card__content {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
Mobile-First Approach:
/* Base styles (mobile) */
.container {
padding: 10px;
font-size: 14px;
}
/* Tablet and up */
@media (min-width: 768px) {
.container {
padding: 20px;
font-size: 16px;
}
}
/* Desktop and up */
@media (min-width: 1024px) {
.container {
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
}
/* Large desktop */
@media (min-width: 1440px) {
.container {
padding: 40px;
max-width: 1400px;
}
}
Common Breakpoints:
/* Mobile: 0-639px (default) */
/* Tablet: 640px-1023px */
@media (min-width: 640px) {}
/* Desktop: 1024px-1279px */
@media (min-width: 1024px) {}
/* Large: 1280px+ */
@media (min-width: 1280px) {}
/* ❌ BAD: Repeated styles */
.button-primary {
padding: 10px 20px;
border-radius: 5px;
font-weight: 600;
background-color: blue;
}
.button-secondary {
padding: 10px 20px;
border-radius: 5px;
font-weight: 600;
background-color: gray;
}
/* ✅ GOOD: Use cascade */
.button {
padding: 10px 20px;
border-radius: 5px;
font-weight: 600;
}
.button-primary {
background-color: blue;
}
.button-secondary {
background-color: gray;
}
/* Modern CSS Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
line-height: 1.5;
font-family: system-ui, -apple-system, sans-serif;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
Or use normalize.css:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
/**
* Component: Card
* Description: Reusable card component for content display
* Last updated: 2024-01-15
*/
.card {
/* ... */
}
/* Section: Header Styles */
.header {
/* Fix for Safari flexbox bug */
min-height: 0;
}
/**
* Color scheme:
* Primary: #1DA1F2
* Secondary: #14171A
* Accent: #F91880
*/
# Use cssnano
npm install -D cssnano postcss-cli
# postcss.config.js
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
})
]
}
<head>
<!-- Inline critical CSS for above-the-fold content -->
<style>
body { margin: 0; font-family: sans-serif; }
.header { background: #fff; height: 60px; }
.hero { min-height: 100vh; }
</style>
<!-- Load remaining CSS asynchronously -->
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="main.css"></noscript>
</head>
/* ❌ EXPENSIVE: Triggers layout/paint */
.expensive {
width: 100px;
height: 100px;
top: 100px;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
/* ✅ CHEAPER: Only triggers composite */
.optimized {
transform: scale(1.1) translateY(10px);
opacity: 0.9;
}
Performance Tiers:
opacity, transform, filter/* ❌ BAD: Overuse creates memory issues */
* {
will-change: transform;
}
/* ✅ GOOD: Only during animation */
.element {
transition: transform 0.3s;
}
.element:hover {
will-change: transform;
transform: translateY(-5px);
}
.element:not(:hover) {
will-change: auto; /* Remove after animation */
}
/* ✅ GOOD: High contrast (WCAG AA: 4.5:1) */
.text-on-dark {
background-color: #000;
color: #fff; /* Contrast: 21:1 */
}
/* ⚠️ WARNING: Low contrast (WCAG fail) */
.text-low-contrast {
background-color: #ccc;
color: #ddd; /* Contrast: 1.5:1 - FAIL */
}
Tools:
/* ❌ BAD: Remove focus outline */
button:focus {
outline: none; /* Never do this without replacement */
}
/* ✅ GOOD: Custom focus style */
button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
/* ✅ BETTER: Use :focus-visible */
button:focus-visible {
outline: 2px solid #4299e1;
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Usage:
<button>
<span class="sr-only">Close modal</span>
<svg>...</svg>
</button>
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Use Autoprefixer - don't write manually:
npm install -D autoprefixer
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
/* Modern feature with fallback */
.element {
background-color: #1DA1F2; /* Fallback */
background-color: oklch(59.69% 0.217 237.04); /* Modern */
}
/* Using @supports */
@supports (display: grid) {
.layout {
display: grid;
}
}
@supports not (display: grid) {
.layout {
display: flex;
}
}
Always verify on Can I Use before using new features. Avoid hardcoding percentages in guidance docs because support changes over time.
Current recommendation:
:has() and subgrid when compatibility is uncertain./* Native CSS nesting (no preprocessor needed) */
.card {
padding: 20px;
& .card-header {
font-size: 24px;
font-weight: bold;
}
& .card-body {
margin-top: 10px;
}
&:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
}
/* Style parent based on child */
.card:has(.card-image) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* Form validation */
.form-group:has(input:invalid) {
border-color: red;
}
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.grid-item {
display: grid;
grid-template-columns: subgrid; /* Inherit parent grid */
}
v3 / v4 / conflict / unknown)?content paths complete? v4: @source added when required?@source)?/* ❌ BAD */
.button { color: blue; }
.button.primary { color: white !important; }
.header .button { color: red !important; }
/* ✅ GOOD */
.button { color: blue; }
.button--primary { color: white; }
.header-button { color: red; }
/* ❌ BAD */
.header { background: #1DA1F2; }
.button { background: #1DA1F2; }
.link { color: #1DA1F2; }
/* ✅ GOOD */
:root { --color-primary: #1DA1F2; }
.header { background: var(--color-primary); }
.button { background: var(--color-primary); }
.link { color: var(--color-primary); }
/* ❌ BAD: Desktop-first */
.container { width: 1200px; padding: 40px; }
@media (max-width: 768px) { .container { width: 100%; padding: 20px; } }
/* ✅ GOOD: Mobile-first */
.container { width: 100%; padding: 20px; }
@media (min-width: 768px) { .container { width: 1200px; padding: 40px; } }
/* ❌ BAD */
.nav ul li a span.icon { /* ... */ }
/* ✅ GOOD */
.nav-icon { /* ... */ }
When providing CSS/Tailwind guidance:
Detected Version: v3 | v4 | conflict | unknownEvidence: dependency + syntax/config evidence you usedPrimary Path: the main recommendation for detected versionAlternate Path: compatible fallback path for the other versionConflict Fix: required when state is conflict (include two repair options + recommended option)Always prioritize:
tools
Use when the user's pain is "adding/removing one more X means editing N files" and X is a recurring variant kind: popup, banner, modal, ad slot, payment method, AI model/tool, form field type, connector, sub-site, command, menu item, agent, extension point, or data source. Use when they want to design, refactor, review, name, or explain a pluggable mechanism using registry, interface/trait contract, runtime core, and convention folders; mention pluginize, pluggable, plugin architecture, extension point, registry pattern, or extensibility. Use when explaining the first-principles rationale, DDD/SOLID/OCP mapping, or industry analogies behind that structure. Use for cross-stack mapping to VSCode contributes, Webpack/Vite plugins, Rust/Tauri connectors, Python entry_points, or cargo features. Skip one variant's internals/styles/hooks/copy/bugs, and skip register/registry meaning DI container, user signup, or package registry.
development
Use BEFORE heavier workflow skills when route choice matters. Route creative work without a design doc/spec to Brainstorm; destructive or hard-to-reverse work to Discuss; unresolved decisions, Plan/Full fan-out, ship checks, unclear bugs, and fresh-eyes fix-then-re-review need this gate. Skip single-line read-only lookups, pure typo/formatting edits, trivial safe one-line fixes, and clearly safe named-skill requests. Outputs Route, Runtime skill, Fallback alias, and Execution path.
development
Cross-agent code review handoff and review-fix-re-review loop with persistent packet artifacts. Requires a git repo because packet addressing uses git rev-parse --show-toplevel. Use when the user asks for an independent, read-only second pair of eyes on a diff/branch/PR another agent or teammate implemented; asks to verify reviewer feedback before fixing; says a fix is done and wants scoped re-review; asks to continue the latest review packet; or asks for first-principles, DDD, high-cohesion/low-coupling review. Persists each loop under $repo_root/.review-handoff/active/ so agents can resume without copy-paste. Do NOT use for ordinary implementation, generic staged-change review, review-comment copy editing, non-git folders/zips/tarballs/temp dirs, or when the user names a different review skill.
testing
Enforces 'decide then plan' discipline - the pre-planning decision gate. Use when the user asks for a plan or starts a change while key decisions are unresolved: architecture tradeoffs, data flow, public interfaces, unclear requirements, multi-module scope, or roughly 5+ files affected. Also triggers when the user explicitly wants to discuss, compare options, or review architecture before committing. Core job: reduce incorrect-execution cost by confirming decisions before producing executable plans.