phy-css-dead-code
Unused CSS detector and dead selector auditor. Scans your stylesheet files against all HTML, JSX, TSX, Vue, and template files to find selectors that are defined but never referenced anywhere in your codebase. Detects CSS framework (Tailwind, Bootstrap, CSS Modules, vanilla CSS, Sass/SCSS) and applies the right analysis strategy for each. Reports estimated byte savings per dead rule, finds !important overuse, duplicate selectors, and orphaned media-query blocks. Generates a safe-deletion plan with exact grep verification commands. Works with PostCSS, Sass, Less, and raw CSS. Zero external API — uses npx and grep only. Triggers on "unused CSS", "dead CSS", "CSS bloat", "remove unused styles", "CSS cleanup", "what CSS can I delete", "/css-dead-code".
安装 / 下载方式
totalclaw install github:LeoYeAI~openclaw-master-skills~phy-css-dead-codecurl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~phy-css-dead-code/file -o phy-css-dead-code.md# CSS Dead Code Auditor
CSS only grows. Every sprint adds new rules; almost none get removed. A feature gets deleted — the styles stay. A component gets renamed — the old class selectors stay. After a year, 30-40% of your stylesheet is dead weight: downloaded by every user, parsed by every browser, doing nothing.
Point this skill at your project and get a complete inventory of dead selectors, their exact file locations, estimated byte savings, and safe-deletion commands.
**Works with any CSS methodology. No config. No build step. Zero external APIs.**
---
## Trigger Phrases
- "unused CSS", "dead CSS selectors", "CSS dead code"
- "CSS bloat", "remove unused styles", "CSS cleanup"
- "what CSS can I delete", "CSS coverage", "unused classes"
- "Tailwind purge", "Bootstrap unused", "CSS audit"
- "stylesheet cleanup", "orphaned selectors"
- "/css-dead-code"
---
## How to Provide Input
```bash
# Option 1: Audit the current directory
/css-dead-code
# Option 2: Specific CSS file or directory
/css-dead-code src/styles/
/css-dead-code assets/main.css
# Option 3: Specify content directory (where HTML/JSX lives)
/css-dead-code --content src/
# Option 4: Specific framework
/css-dead-code --framework tailwind
/css-dead-code --framework vanilla
/css-dead-code --framework bootstrap
# Option 5: Show only selectors that can be safely deleted (high confidence)
/css-dead-code --safe-only
# Option 6: Estimate size savings only (quick mode)
/css-dead-code --size-estimate
# Option 7: Check a single selector
/css-dead-code --check ".btn-legacy-primary"
```
---
## Step 1: Detect CSS Framework and Project Structure
```bash
# Detect CSS framework from project files
FRAMEWORK="unknown"
if [ -f "tailwind.config.js" ] || [ -f "tailwind.config.ts" ] || [ -f "tailwind.config.cjs" ]; then
FRAMEWORK="tailwind"
elif grep -r "bootstrap" package.json 2>/dev/null | grep -q "\"bootstrap\""; then
FRAMEWORK="bootstrap"
elif find . -name "*.module.css" -not -path "*/node_modules/*" 2>/dev/null | head -1 | grep -q "."; then
FRAMEWORK="css-modules"
elif find . -name "*.scss" -o -name "*.sass" -not -path "*/node_modules/*" 2>/dev/null | head -3 | grep -q "."; then
FRAMEWORK="sass"
else
FRAMEWORK="vanilla"
fi
echo "Detected framework: $FRAMEWORK"
# Find all CSS/SCSS/SASS/Less files
CSS_FILES=$(find . \( -name "*.css" -o -name "*.scss" -o -name "*.sass" -o -name "*.less" \) \
-not -path "*/node_modules/*" \
-not -path "*/dist/*" \
-not -path "*/.next/*" \
-not -path "*/build/*" \
2>/dev/null)
CSS_COUNT=$(echo "$CSS_FILES" | wc -l | tr -d ' ')
echo "Found $CSS_COUNT CSS/SCSS files"
# Find all template/component files
CONTENT_FILES=$(find . \( -name "*.html" -o -name "*.jsx" -o -name "*.tsx" \
-o -name "*.vue" -o -name "*.svelte" -o -name "*.hbs" \
-o -name "*.erb" -o -name "*.blade.php" -o -name "*.pug" \) \
-not -path "*/node_modules/*" \
-not -path "*/dist/*" \
2>/dev/null)
CONTENT_COUNT=$(echo "$CONTENT_FILES" | wc -l | tr -d ' ')
echo "Found $CONTENT_COUNT template/component files to cross-reference"
```
---
## Step 2: Framework-Specific Analysis
### Tailwind CSS
```bash
# Check tailwind.config.js content paths
echo "=== Tailwind Config Check ==="
python3 -c "
import re, json
try:
with open('tailwind.config.js') as f:
content = f.read()
# Extract content array
content_match = re.search(r'content\s*:\s*\[(.*?)\]', content, re.DOTALL)
if content_match:
print('Content paths configured:')
for line in content_match.group(1).split(','):
line = line.strip().strip('\"\'')
if line:
print(f' {line}')
else:
print('⚠️ No content array found in tailwind.config.js — all classes may be purged!')
except FileNotFoundError:
print('tailwind.config.js not found')
"
# Find classes actually used in content files
echo ""
echo "=== Tailwind Classes In Use ==="
# Extract all class="" values from content files
grep -rh "class[Name]*=\"[^\"]*\"\|class[Name]*={[^}]*}" . \
--include="*.jsx" --include="*.tsx" --include="*.html" --include="*.vue" \
--exclude-dir="node_modules" --exclude-dir="dist" 2>/dev/null | \
grep -oE '[a-z][a-z0-9:-]+' | sort -u | head -50
# Run Tailwind CLI to see what would be purged
if command -v npx &>/dev/null; then
echo ""
echo "Running Tailwind dry-run to check purged classes..."
npx --yes tailwindcss -i ./input.css -o /tmp/tw-output.css --minify 2>/dev/null
echo "Output size: $(du -h /tmp/tw-output.css 2>/dev/null | cut -f1)"
fi
```
### Vanilla CSS / SCSS
```python
# Extract all selectors from CSS files and cross-reference with content
import re, os, sys
from pathlib import Path
from collections import defaultdict
def extract_selectors(css_content):
"""Extract all CSS selectors from a stylesheet."""
# Remove comments
css = re.sub(r'/\*.*?\*/', '', css_content, flags=re.DOTALL)
# Remove @-rules (media queries, keyframes) content for now
css = re.sub(r'@[^{]+\{[^}]+\}', '', css)
# Extract selectors (before the { block)
selectors = re.findall(r'([^{}@][^{}]*)\s*\{', css)
result = []
for sel_group in selectors:
# Split on commas for multi-selector rules
for sel in sel_group.split(','):
sel = sel.strip()
if sel and not sel.startswith('@'):
result.append(sel)
return result
def extract_class_refs(content):
"""Extract all class and ID references from template/component files."""
classes = set()
ids = set()
# class="foo bar baz"
for match in re.finditer(r'class(?:Name)?\s*=\s*["\']([^"\']+)["\']', content):
classes.update(match.group(1).split())
# className={`...`} dynamic classes
for match in re.finditer(r'["\']([a-z][a-z0-9-_]+)["\']', content):
classes.add(match.group(1))
# id="foo"
for match in re.finditer(r'\bid\s*=\s*["\']([^"\']+)["\']', content):
ids.add(match.group(1))
return classes, ids
# Collect all selectors from CSS files
all_selectors = defaultdict(list) # selector -> [(file, line_num)]
css_dirs = ['.']
for css_dir in css_dirs:
for fpath in Path(css_dir).rglob('*.css'):
if 'node_modules' in str(fpath) or 'dist' in str(fpath):
continue
content = fpath.read_text(errors='ignore')
for sel in extract_selectors(content):
all_selectors[sel].append(str(fpath))
print(f"Total unique selectors found: {len(all_selectors)}")
# Collect all class/id references from content files
all_classes = set()
all_ids = set()
content_exts = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.erb', '.hbs']
for root, dirs, files in os.walk('.'):
dirs[:] = [d for d in dirs if d not in {'node_modules', 'dist', '.next', 'build', '.git'}]
for fname in files:
if any(fname.endswith(ext) for ext in content_exts):
fpath = Path(root) / fname
content = fpath.read_text(errors='ignore')
c, i = extract_class_refs(content)
all_classes.update(c)
all_ids.update(i)
print(f"Unique class references found in templates: {len(all_classes)}")
# Find unused selectors
unused = []
for selector, locations in all_selectors.items():
# Extract the class name from the selector
class_matches = re.findall(r'\.([a-zA-Z][a-zA-Z0-9_-]*)', selector)
id_matches = re.findall(r'#([a-zA-Z][a-zA-Z0-9_-]*)', selector)
is_used = False
for cls in class_matches:
if cls in all_classes:
is_used = True
break
for id_name in id_matches:
if id_name in all_ids:
is_used = True
break
# Skip element-only selectors (body, h1, p, etc.) — too risky to flag
if not class_matches and not id_matches:
continue # element selector, skip
if not is_used:
unused.append((selector, locations))
print(f"\nUnused selectors: {len(unused)}")
for sel, locs in sorted(unused, key=lambda x: len(x[1]), reverse=True)[:3