tailwind-conventions
Tailwind class ordering, design tokens, and when to extract components vs. utilities
Install
Or scan + install everything matching your stack with npx autoskills.
Source
View on GitHubTailwind Conventions
Tailwind only stays maintainable if you have conventions. Without them, every component becomes a 400-character className string nobody can diff.
Class ordering
Use Prettier with prettier-plugin-tailwindcss. It sorts classes deterministically. No team debates about order.
Logical groups, in this order (the plugin's default):
- Layout โ
block,flex,grid,hidden,inline-flex - Positioning โ
relative,absolute,top-0,inset-x-0,z-10 - Box model โ
m-4,p-2,w-full,h-screen - Border โ
border,rounded-lg,border-gray-200 - Background โ
bg-white,bg-gradient-to-r - Typography โ
text-sm,font-medium,leading-tight - Effects โ
shadow,opacity-50,blur-sm - Transitions โ
transition,duration-150 - Interactivity โ
cursor-pointer,select-none - States โ
hover:bg-blue-600,focus:ring-2,disabled:opacity-50 - Responsive โ
md:flex,lg:p-8
Design tokens, not arbitrary values
<!-- bad: magic numbers everywhere -->
<div class="text-[14.5px] mt-[13px] bg-[#3b82f6]">
<!-- good: extends to the design system -->
<div class="text-sm mt-3 bg-blue-500">If you need a non-default value, add it to tailwind.config.js, don't use [arbitrary] inline. Arbitrary values are escape hatches; using them everywhere means there is no system.
When to extract a component vs. a utility
- Use a component (React/Vue/Blade) for repeated UI, not
@apply.@applycreates a CSS layer Tailwind has to scan; components are how your framework thinks. - Use
@applyonly for primitive design tokens shared across the app (e.g..prose-default,.input-base) โ and only in a CSS file, not inline. - Don't extract too early. Three uses of similar classes โ a component yet. Wait for the abstraction to be obvious.
Conditional classes
Use clsx or tailwind-merge for conditionals. Don't string-concatenate.
// good
import { cn } from '@/lib/utils'; // re-exports clsx + twMerge
<button className={cn(
'rounded px-4 py-2',
isPrimary ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-900',
isDisabled && 'opacity-50 cursor-not-allowed',
className, // allow override
)}>tailwind-merge resolves conflicts: cn('p-4', 'p-6') โ 'p-6'. Critical for component prop overrides.
Dark mode
darkMode: 'class'intailwind.config.jsโ gives you adark:variant gated by a.darkclass on<html>.- Don't sprinkle
dark:everywhere. Define dark mode at the design token level when possible (bg-background/text-foregroundwith CSS vars).
Spacing
- Stick to the default scale (
p-1top-32). Custom values likep-[7px]are red flags. - Use
space-y-*/space-x-*for vertical/horizontal rhythm in flex/grid children. Cleaner than margin-on-each-child. gap-*on flex/grid containers is preferable tospace-*for modern layouts.
Common anti-patterns
- Inline
style={{}}for things Tailwind already has โ pick Tailwind or pick inline, not both. - Mixing Tailwind with utility libraries that fight it โ Bootstrap, MUI, etc. Pick one design system.
- 400+ char
classNameโ extract a component. - Repeating the exact same class string in 5 places โ extract a component.
- Using
!important(!bg-blue-500) โ almost always a sign you have a specificity conflict you should fix instead.
Quick check before commit
- Did Prettier sort classes? (run it, don't argue manually)
- Any arbitrary values? If yes, should they be in the config?
- Same long string in multiple files? Extract.
- Any
!important(!)? Why?