Automatically detect and fix CSS inconsistencies, conflicts, and suggest maintainable styling patterns for Tailwind, SCSS, and CSS Modules
1. Conflicting Styles
/* ❌ Problem: Specificity conflict */
.button { background: blue; }
button.button { background: red; }
#header .button { background: green; }
/* ✅ Fixed: Consistent specificity */
.button { background: var(--button-bg, blue); }
.button--primary { background: var(--button-primary-bg, red); }
.button--success { background: var(--button-success-bg, green); }
2. Duplicated Properties
/* ❌ Problem: Repeated values */
.card { margin: 20px; padding: 20px; }
.modal { margin: 20px; padding: 20px; }
.panel { margin: 20px; padding: 20px; }
/* ✅ Fixed: Use CSS variables or utility classes */
:root {
--spacing-medium: 20px;
}
.card, .modal, .panel {
margin: var(--spacing-medium);
padding: var(--spacing-medium);
}
3. Inconsistent Naming
/* ❌ Problem: Mixed naming conventions */
.btn-Primary { }
.button_secondary { }
.ButtonTertiary { }
/* ✅ Fixed: BEM or consistent naming */
.button--primary { }
.button--secondary { }
.button--tertiary { }
// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-prettier'
],
plugins: [
'stylelint-order',
'stylelint-scss'
],
rules: {
// Enforce consistent naming
'selector-class-pattern': '^[a-z][a-z0-9]*(-[a-z0-9]+)*$',
// Prevent duplication
'declaration-block-no-duplicate-properties': true,
'no-duplicate-selectors': true,
// Enforce property order
'order/properties-alphabetical-order': true,
// Limit specificity
'selector-max-id': 0,
'selector-max-specificity': '0,3,0',
// Prevent redundant
'shorthand-property-no-redundant-values': true,
'declaration-block-no-redundant-longhand-properties': true,
// Color consistency
'color-named': 'never',
'color-no-hex': true,
// Units
'length-zero-no-unit': true,
'unit-allowed-list': ['px', 'rem', '%', 'vh', 'vw', 's', 'ms'],
// No vendor prefixes (use autoprefixer)
'property-no-vendor-prefix': true,
'value-no-vendor-prefix': true
}
};
// scripts/analyze-css.js
import fs from 'fs';
import path from 'path';
import postcss from 'postcss';
class CSSAnalyzer {
constructor() {
this.issues = [];
this.stats = {
totalRules: 0,
totalDeclarations: 0,
duplicateSelectors: new Map(),
duplicateDeclarations: new Map(),
colorVariations: new Map(),
spacingValues: new Map()
};
}
async analyzeFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const ast = postcss.parse(content);
ast.walkRules(rule => {
this.stats.totalRules++;
this.analyzeRule(rule, filePath);
});
return this.generateReport();
}
analyzeRule(rule, file) {
const selector = rule.selector;
// Check for duplicate selectors
if (!this.stats.duplicateSelectors.has(selector)) {
this.stats.duplicateSelectors.set(selector, []);
}
this.stats.duplicateSelectors.get(selector).push(file);
// Analyze declarations
rule.walkDecls(decl => {
this.stats.totalDeclarations++;
this.analyzeDeclaration(decl, selector, file);
});
}
analyzeDeclaration(decl, selector, file) {
const prop = decl.prop;
const value = decl.value;
// Track color values
if (this.isColor(prop)) {
if (!this.stats.colorVariations.has(value)) {
this.stats.colorVariations.set(value, []);
}
this.stats.colorVariations.get(value).push({ selector, file });
}
// Track spacing values
if (this.isSpacing(prop)) {
if (!this.stats.spacingValues.has(value)) {
this.stats.spacingValues.set(value, []);
}
this.stats.spacingValues.get(value).push({ selector, prop, file });
}
// Check for redundant declarations
const key = `${selector}:${prop}`;
if (!this.stats.duplicateDeclarations.has(key)) {
this.stats.duplicateDeclarations.set(key, []);
}
this.stats.duplicateDeclarations.get(key).push({ value, file });
}
isColor(prop) {
return ['color', 'background-color', 'border-color', 'fill', 'stroke']
.includes(prop);
}
isSpacing(prop) {
return ['margin', 'padding', 'gap', 'margin-top', 'margin-right',
'margin-bottom', 'margin-left', 'padding-top', 'padding-right',
'padding-bottom', 'padding-left'].includes(prop);
}
generateReport() {
const report = {
summary: {
totalRules: this.stats.totalRules,
totalDeclarations: this.stats.totalDeclarations,
duplicateSelectorsCount: 0,
uniqueColors: this.stats.colorVariations.size,
uniqueSpacingValues: this.stats.spacingValues.size
},
issues: [],
suggestions: []
};
// Find duplicate selectors
this.stats.duplicateSelectors.forEach((files, selector) => {
if (files.length > 1) {
report.summary.duplicateSelectorsCount++;
report.issues.push({
type: 'duplicate-selector',
selector,
files,
severity: 'warning'
});
}
});
// Suggest color variables
if (this.stats.colorVariations.size > 5) {
const colors = Array.from(this.stats.colorVariations.entries())
.filter(([_, usages]) => usages.length > 2)
.map(([color, usages]) => ({ color, usages: usages.length }));
if (colors.length > 0) {
report.suggestions.push({
type: 'use-color-variables',
message: 'Consider using CSS variables for frequently used colors',
colors
});
}
}
// Suggest spacing scale
if (this.stats.spacingValues.size > 8) {
report.suggestions.push({
type: 'use-spacing-scale',
message: 'Consider implementing a consistent spacing scale',
uniqueValues: this.stats.spacingValues.size
});
}
return report;
}
}
// Usage
const analyzer = new CSSAnalyzer();
const report = await analyzer.analyzeFile('./src/styles/main.css');
console.log('📊 CSS Analysis Report\n');
console.log(`Total Rules: ${report.summary.totalRules}`);
console.log(`Total Declarations: ${report.summary.totalDeclarations}`);
console.log(`Duplicate Selectors: ${report.summary.duplicateSelectorsCount}`);
console.log(`Unique Colors: ${report.summary.uniqueColors}`);
console.log(`Unique Spacing Values: ${report.summary.uniqueSpacingValues}\n`);
if (report.issues.length > 0) {
console.log('⚠️ Issues Found:\n');
report.issues.forEach(issue => {
console.log(`- ${issue.type}: ${issue.selector}`);
});
}
if (report.suggestions.length > 0) {
console.log('\n💡 Suggestions:\n');
report.suggestions.forEach(suggestion => {
console.log(`- ${suggestion.message}`);
});
}
// scripts/convert-to-tailwind.js
class TailwindConverter {
constructor() {
this.styleMap = {
// Layout
'display: flex': 'flex',
'display: grid': 'grid',
'display: block': 'block',
'display: none': 'hidden',
// Spacing
'margin: 0': 'm-0',
'margin: 4px': 'm-1',
'margin: 8px': 'm-2',
'padding: 0': 'p-0',
'padding: 4px': 'p-1',
'padding: 8px': 'p-2',
// Colors
'color: #000': 'text-black',
'color: #fff': 'text-white',
'background-color: #000': 'bg-black',
'background-color: #fff': 'bg-white',
// Typography
'font-weight: bold': 'font-bold',
'text-align: center': 'text-center',
'font-size: 14px': 'text-sm',
'font-size: 16px': 'text-base',
'font-size: 18px': 'text-lg'
};
}
convertInlineStyles(jsx) {
const inlineStyleRegex = /style={{([^}]+)}}/g;
const matches = jsx.matchAll(inlineStyleRegex);
const conversions = [];
for (const match of matches) {
const styles = match[1];
const tailwindClasses = this.convertToTailwind(styles);
conversions.push({
original: match[0],
tailwind: `className="${tailwindClasses}"`,
styles: styles.trim()
});
}
return conversions;
}
convertToTailwind(styles) {
const declarations = styles
.split(',')
.map(s => s.trim().replace(/['"]/g, ''));
const classes = [];
declarations.forEach(decl => {
const normalized = this.normalizeDeclaration(decl);
const tailwindClass = this.styleMap[normalized];
if (tailwindClass) {
classes.push(tailwindClass);
} else {
// Try to infer Tailwind class
const inferred = this.inferTailwindClass(decl);
if (inferred) {
classes.push(inferred);
}
}
});
return classes.join(' ');
}
normalizeDeclaration(decl) {
return decl
.replace(/:\s+/g, ': ')
.replace(/;$/, '')
.trim();
}
inferTailwindClass(decl) {
const [prop, value] = decl.split(':').map(s => s.trim());
// Margin/Padding with px values
if (prop.startsWith('margin') || prop.startsWith('padding')) {
const match = value.match(/(\d+)px/);
if (match) {
const px = parseInt(match[1]);
const scale = Math.round(px / 4); // Tailwind uses 4px scale
const prefix = prop.startsWith('margin') ? 'm' : 'p';
const side = this.getSide(prop);
return `${prefix}${side}-${scale}`;
}
}
return null;
}
getSide(prop) {
if (prop.includes('top')) return 't';
if (prop.includes('right')) return 'r';
if (prop.includes('bottom')) return 'b';
if (prop.includes('left')) return 'l';
return '';
}
}
// Usage in JSX file
const converter = new TailwindConverter();
const jsxContent = fs.readFileSync('./src/Component.jsx', 'utf8');
const conversions = converter.convertInlineStyles(jsxContent);
console.log('🎨 Tailwind Conversion Suggestions:\n');
conversions.forEach(conv => {
console.log(`Replace:`);
console.log(` ${conv.original}`);
console.log(`With:`);
console.log(` ${conv.tailwind}\n`);
});
// scripts/detect-unused-tailwind.js
import fs from 'fs';
import path from 'path';
class TailwindAuditor {
constructor() {
this.usedClasses = new Set();
this.definedClasses = new Set();
}
async audit(srcDir, configPath) {
// Scan all JSX/TSX files for used classes
this.scanForUsedClasses(srcDir);
// Load Tailwind config to see what's defined
this.loadTailwindConfig(configPath);
// Find unused
const unused = Array.from(this.definedClasses)
.filter(cls => !this.usedClasses.has(cls));
return {
totalDefined: this.definedClasses.size,
totalUsed: this.usedClasses.size,
unused
};
}
scanForUsedClasses(dir) {
const files = this.getAllFiles(dir);
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
// Extract className attributes
const classNameRegex = /className="([^"]+)"|className={'([^']+)'}/g;
const matches = content.matchAll(classNameRegex);
for (const match of matches) {
const classes = (match[1] || match[2]).split(/\s+/);
classes.forEach(cls => this.usedClasses.add(cls.trim()));
}
});
}
loadTailwindConfig(configPath) {
// This would parse tailwind.config.js
// For simplicity, we'll just note common classes
const commonClasses = [
'flex', 'grid', 'hidden', 'block',
'm-0', 'm-1', 'm-2', 'p-0', 'p-1', 'p-2',
'text-black', 'text-white', 'bg-black', 'bg-white'
];
commonClasses.forEach(cls => this.definedClasses.add(cls));
}
getAllFiles(dir, ext = ['.jsx', '.tsx']) {
// Implementation similar to previous examples
}
}
/* ❌ Bad SCSS practices */
.header {
.nav {
.item {
.link {
.icon {
/* Too deeply nested */
}
}
}
}
}
/* ✅ Good: Limit nesting to 3 levels */
.header {}
.header__nav {}
.header__nav-item {}
.header__nav-link {}
.header__nav-icon {}
/* ❌ Bad: Hardcoded colors */
.button {
background: #3498db;
color: #ffffff;
}
/* ✅ Good: Use variables */
$color-primary: #3498db;
$color-white: #ffffff;
.button {
background: $color-primary;
color: $color-white;
}
/* ❌ Bad: Magic numbers */
.card {
padding: 23px;
margin: 17px;
}
/* ✅ Good: Named spacing scale */
$spacing: (
xs: 4px,
sm: 8px,
md: 16px,
lg: 24px,
xl: 32px
);
.card {
padding: map-get($spacing, lg);
margin: map-get($spacing, md);
}
// scripts/fix-scss.js
import fs from 'fs';
import postcss from 'postcss';
import scss from 'postcss-scss';
class SCSSFixer {
constructor() {
this.fixes = [];
}
async fixFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const ast = postcss().process(content, { syntax: scss });
ast.root.walkRules(rule => {
// Fix 1: Limit nesting depth
const depth = this.getNestingDepth(rule);
if (depth > 3) {
this.fixes.push({
type: 'deep-nesting',
selector: rule.selector,
depth,
suggestion: 'Flatten using BEM or separate selectors'
});
}
// Fix 2: Extract hardcoded colors
rule.walkDecls(decl => {
if (this.isColor(decl.prop) && this.isHexColor(decl.value)) {
this.fixes.push({
type: 'hardcoded-color',
property: decl.prop,
value: decl.value,
suggestion: `Use variable: $color-${this.colorName(decl.value)}`
});
}
});
});
return this.fixes;
}
getNestingDepth(rule, depth = 0) {
return rule.parent && rule.parent.type === 'rule'
? this.getNestingDepth(rule.parent, depth + 1)
: depth;
}
isColor(prop) {
return ['color', 'background-color', 'border-color'].includes(prop);
}
isHexColor(value) {
return /^#[0-9a-f]{3,6}$/i.test(value);
}
colorName(hex) {
// Simple color naming (would be more sophisticated in production)
const colorMap = {
'#000': 'black',
'#fff': 'white',
'#3498db': 'primary',
'#e74c3c': 'danger'
};
return colorMap[hex.toLowerCase()] || hex.slice(1);
}
}
// ❌ Bad: Global class pollution
import './styles.css';
function Component() {
return <div className="container" />;
}
// ✅ Good: Scoped CSS Modules
import styles from './Component.module.css';
function Component() {
return <div className={styles.container} />;
}
// scripts/convert-to-css-modules.js
class CSSModuleConverter {
async convertFile(cssPath) {
const content = fs.readFileSync(cssPath, 'utf8');
const componentName = path.basename(cssPath, '.css');
const modulePath = cssPath.replace('.css', '.module.css');
// Convert global classes to BEM
const converted = content.replace(
/\.([a-z-]+)\s*{/g,
`.${componentName}__$1 {`
);
fs.writeFileSync(modulePath, converted);
return {
original: cssPath,
module: modulePath,
suggestion: `Update imports to use: import styles from './${path.basename(modulePath)}'`
};
}
}
// Using PurgeCSS
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
purgecss({
content: ['./src/**/*.{js,jsx,ts,tsx}'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
};
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true
}]
})
]
};
{
"scripts": {
"lint:css": "stylelint 'src/**/*.{css,scss}'",
"lint:css:fix": "stylelint 'src/**/*.{css,scss}' --fix",
"analyze:css": "node scripts/analyze-css.js",
"convert:tailwind": "node scripts/convert-to-tailwind.js",
"audit:tailwind": "node scripts/detect-unused-tailwind.js",
"fix:scss": "node scripts/fix-scss.js"
}
}