Custom Rules
Custom rules allow you to extend Headwind with your own utility patterns and CSS properties beyond the built-in utilities.
Overview
While Headwind includes comprehensive built-in utilities, you can add custom rules to:
- Create domain-specific utilities
- Add vendor-prefixed properties
- Support experimental CSS features
- Implement custom design patterns
Basic Custom Rules
Define custom rules in your configuration:
import type { HeadwindOptions } from 'headwind'
const config = {
rules: [
// Static rule
[
/^custom-utility$/,
() => ({
'custom-property': 'value',
}),
],
// Dynamic rule with pattern matching
[
/^custom-(\w+)$/,
match => ({
'custom-property': match[1],
}),
],
],
} satisfies HeadwindOptions
export default configRule Format
Each custom rule is a tuple:
type CustomRule = [
RegExp, // Pattern to match
(match: RegExpMatchArray) => Record<string, string> | undefined
]Pattern (RegExp)
The first element is a regular expression that matches utility class names:
/^my-utility-(\d+)$/ // Matches: my-utility-1, my-utility-24, etc.
/^custom-(\w+)$/ // Matches: custom-red, custom-blue, etc.
/^prefix-/ // Matches: anything starting with prefix-Handler Function
The second element is a function that returns CSS properties:
(match: RegExpMatchArray) => {
return {
'property-name': 'value',
'another-property': 'another-value',
}
}Return undefined to skip generating CSS for the match.
Examples
Simple Static Rule
const config = {
rules: [
[
/^glass$/,
() => ({
'background': 'rgba(255, 255, 255, 0.1)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(255, 255, 255, 0.2)',
}),
],
],
}Usage:
<div class="glass">Glassmorphism effect</div>Dynamic Numeric Values
const config = {
rules: [
[
/^aspect-(\d+)\/(\d+)$/,
match => ({
'aspect-ratio': `${match[1]} / ${match[2]}`,
}),
],
],
}Usage:
<div class="aspect-16/9">16:9 aspect ratio</div>
<div class="aspect-4/3">4:3 aspect ratio</div>Color Utilities
const config = {
rules: [
[
/^brand-(\w+)$/,
(match) => {
const colors: Record<string, string> = {
primary: '#3b82f6',
secondary: '#8b5cf6',
accent: '#f59e0b',
}
const color = colors[match[1]]
if (!color)
return undefined
return {
color,
}
},
],
],
}Usage:
<span class="brand-primary">Primary brand color</span>
<span class="brand-secondary">Secondary brand color</span>Grid Template Utilities
const config = {
rules: [
[
/^grid-areas-(\w+)$/,
(match) => {
const templates: Record<string, string> = {
header: '"header header" "nav main" "footer footer"',
sidebar: '"sidebar main" "sidebar footer"',
dashboard: '"header header header" "nav content aside" "footer footer footer"',
}
const template = templates[match[1]]
if (!template)
return undefined
return {
'grid-template-areas': template,
}
},
],
],
}Usage:
<div class="grid grid-areas-header">
<header class="grid-area-[header]">Header</header>
<nav class="grid-area-[nav]">Nav</nav>
<main class="grid-area-[main]">Main</main>
</div>Animation Utilities
const config = {
rules: [
[
/^animate-delay-(\d+)$/,
match => ({
'animation-delay': `${match[1]}ms`,
}),
],
[
/^animate-duration-(\d+)$/,
match => ({
'animation-duration': `${match[1]}ms`,
}),
],
],
}Usage:
<div class="animate-delay-500 animate-duration-1000">
Delayed animation
</div>Advanced Patterns
Multiple Properties
Generate multiple CSS properties from a single utility:
const config = {
rules: [
[
/^truncate-(\d+)$/,
match => ({
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
'max-width': `${match[1]}ch`,
}),
],
],
}Conditional Logic
Use conditional logic in handlers:
const config = {
rules: [
[
/^size-(\w+)$/,
(match) => {
const value = match[1]
// Custom size mapping
if (value === 'xs') {
return { width: '16px', height: '16px' }
}
if (value === 'sm') {
return { width: '24px', height: '24px' }
}
if (value === 'md') {
return { width: '32px', height: '32px' }
}
if (value === 'lg') {
return { width: '48px', height: '48px' }
}
return undefined
},
],
],
}Using Theme Values
Access theme values in custom rules:
import type { HeadwindOptions } from 'headwind'
const config = {
theme: {
spacing: {
xs: '0.5rem',
sm: '1rem',
md: '2rem',
lg: '4rem',
},
},
rules: [
[
/^gap-custom-(\w+)$/,
(match) => {
const spacingKey = match[1]
const spacing = config.theme.spacing[spacingKey]
if (!spacing)
return undefined
return {
gap: spacing,
}
},
],
],
} satisfies HeadwindOptionsVendor Prefixes
Add vendor prefixes for experimental features:
const config = {
rules: [
[
/^backdrop-blur-(\d+)$/,
(match) => {
const value = `blur(${match[1]}px)`
return {
'-webkit-backdrop-filter': value,
'backdrop-filter': value,
}
},
],
],
}Child Selectors
Return utilities that affect child elements:
const config = {
rules: [
[
/^stack-(\d+)$/,
match => ({
'> * + *': {
'margin-top': `${match[1] * 0.25}rem`,
},
}),
],
],
}Complex Examples
Glassmorphism Utility
const config = {
rules: [
[
/^glass-(\w+)$/,
(match) => {
const variants: Record<string, any> = {
light: {
'background': 'rgba(255, 255, 255, 0.1)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(255, 255, 255, 0.2)',
},
dark: {
'background': 'rgba(0, 0, 0, 0.1)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(0, 0, 0, 0.2)',
},
colored: {
'background': 'rgba(59, 130, 246, 0.1)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(59, 130, 246, 0.2)',
},
}
return variants[match[1]]
},
],
],
}Gradient Utilities
const config = {
rules: [
[
/^gradient-(\w+)-to-(\w+)$/,
(match) => {
const from = match[1]
const to = match[2]
const colors: Record<string, string> = {
red: '#ef4444',
blue: '#3b82f6',
green: '#10b981',
purple: '#8b5cf6',
pink: '#ec4899',
}
const fromColor = colors[from]
const toColor = colors[to]
if (!fromColor || !toColor)
return undefined
return {
'background-image': `linear-gradient(to right, ${fromColor}, ${toColor})`,
}
},
],
],
}Usage:
<div class="gradient-blue-to-purple">
Blue to purple gradient
</div>Responsive Font Sizes
const config = {
rules: [
[
/^fluid-text-(\w+)$/,
(match) => {
const sizes: Record<string, string> = {
sm: 'clamp(0.875rem, 2vw, 1rem)',
md: 'clamp(1rem, 2.5vw, 1.25rem)',
lg: 'clamp(1.25rem, 3vw, 1.875rem)',
xl: 'clamp(1.875rem, 4vw, 3rem)',
}
const size = sizes[match[1]]
if (!size)
return undefined
return {
'font-size': size,
}
},
],
],
}Best Practices
1. Use Specific Patterns
✅ Good:
/^my-utility-(\d+)$/ // Specific pattern❌ Avoid:
/^my-/ // Too broad, might conflict2. Validate Input
const config = {
rules: [
[
/^custom-(\d+)$/,
(match) => {
const value = Number.parseInt(match[1])
// Validate range
if (value < 0 || value > 100) {
return undefined
}
return {
'custom-property': `${value}%`,
}
},
],
],
}3. Return Undefined for Invalid Values
const config = {
rules: [
[
/^size-(\w+)$/,
(match) => {
const validSizes = ['sm', 'md', 'lg']
if (!validSizes.includes(match[1])) {
return undefined // Don't generate CSS
}
return { /* ... */ }
},
],
],
}4. Document Your Rules
const config = {
rules: [
// Custom aspect ratio utility
// Usage: aspect-16/9, aspect-4/3
[
/^aspect-(\d+)\/(\d+)$/,
match => ({
'aspect-ratio': `${match[1]} / ${match[2]}`,
}),
],
// Glassmorphism effect
// Usage: glass-light, glass-dark
[
/^glass-(\w+)$/,
(match) => {
// Implementation
},
],
],
}5. Namespace Custom Utilities
// Prefix with your project/brand name
/^acme-(\w+)$/ // Acme Corp utilities
/^myapp-(\w+)$/ // MyApp utilities
/^custom-(\w+)$/ // Generic custom utilitiesPerformance
Custom rules have minimal performance impact:
- Matching: Regex matching is fast
- Generation: Only matched classes generate CSS
- Output: Same as built-in utilities
Optimization Tips
Use specific patterns:
typescript// ✅ Fast /^my-utility-(\d+)$/ // ❌ Slower (too broad) /^my-/Avoid complex logic:
typescript// ✅ Simple check if (value === 'sm') return { /* ... */ } // ❌ Complex computation if (expensiveFunction(value)) return { /* ... */ }Cache computed values:
typescriptconst computedValues = new Map() const rule = [ /^custom-(\w+)$/, (match) => { const key = match[1] if (computedValues.has(key)) { return computedValues.get(key) } const result = { /* computed properties */ } computedValues.set(key, result) return result }, ]
Troubleshooting
Rule Not Matching
Check:
Pattern is correct:
typescript// Test pattern /^my-utility-(\d+)$/.test('my-utility-10') // trueClass name matches exactly:
html<div class="my-utility-10">✅ Matches</div> <div class="my-utility-abc">❌ No match</div>Handler returns valid object:
typescript(match) => { console.log('Match:', match) return { /* properties */ } }
No CSS Generated
Check:
- Handler returns object (not undefined)
- Properties are valid CSS
- Rule is added to config array
Conflicts with Built-in Utilities
Solution: Use unique prefixes:
// ❌ Conflicts with built-in 'flex'
/^flex$/
// ✅ Unique prefix
/^custom-flex$/Related
- Configuration - Full configuration guide
- Shortcuts - Reusable utility combinations
- Presets - Share custom rules across projects
- TypeScript - Type-safe custom rules