Type-safe CSS class constants for Go/templ projects.
cssgen generates Go constants from your CSS files and provides a linter to eliminate hardcoded class strings and catch typos at build time.
- Why cssgen?
- Features
- Installation
- Quick Start
- Examples
- Generated Output
- Linting Philosophy
- Output Formats
- Usage Examples
- How It Works
- Configuration
- FAQ
- License
- Contributing
In modern Go web development with templ, CSS class names are just strings. This creates two problems:
- Typos are runtime errors -
class="btn btn--primray"fails silently - No refactoring support - Renaming
.btn-primaryto.btn--brandrequires manual find/replace
cssgen solves this with type-safe constants and build-time validation:
// Before: Runtime error waiting to happen
<button class="btn btn--primray">Click</button>
// After: Compile-time safety + IDE autocomplete
<button class={ ui.Btn, ui.BtnBrand }>Click</button>- ✅ 1:1 CSS-to-Go mapping - One CSS class = One Go constant
- ✅ Rich IDE tooltips - Hover over
ui.BtnBrandto see CSS properties, layers, and inheritance - ✅ Smart linter - Detects typos (errors) and hardcoded strings (warnings)
- ✅ Multiple output formats - Issues, JSON, Markdown reports
- ✅ Zero runtime overhead - Pure compile-time tool
- ✅ Component-based generation - Splits constants into logical files (buttons, cards, etc.)
go install github.com/yacobolo/cssgen/cmd/cssgen@latestRequirements: Go 1.21+
cssg -source ./web/styles -output-dir ./internal/ui -package uiThis scans your CSS files and generates:
internal/ui/
├── styles.gen.go # Main file with AllCSSClasses registry
├── styles_buttons.gen.go # Button constants
├── styles_cards.gen.go # Card constants
└── ... # Other component files
import "yourproject/internal/ui"
templ Button(text string) {
<button class={ ui.Btn, ui.BtnBrand, ui.BtnLg }>
{ text }
</button>
}
// Produces: <button class="btn btn--brand btn--lg"># Default: Show errors and warnings (golangci-lint style)
cssg -lint-only
# CI mode: Fail on any issue
cssg -lint-only -strict
# Full report with statistics and Quick Wins
cssg -lint-only -output-format fullThe examples/ directory contains comprehensive examples showing how cssgen transforms CSS into type-safe Go constants:
| Example | Focus | Complexity |
|---|---|---|
| 01-basic | Simple button styles with BEM modifiers | ⭐ Beginner |
| 02-bem-methodology | Comprehensive BEM patterns | ⭐⭐ Intermediate |
| 03-component-library | Production-ready UI components | ⭐⭐⭐ Advanced |
| 04-css-layers | CSS cascade layers (@layer) | ⭐⭐ Intermediate |
| 05-utility-first | Utility class patterns (Tailwind-style) | ⭐⭐ Intermediate |
| 06-complex-selectors | Advanced CSS selector handling | ⭐⭐⭐ Advanced |
New to cssgen? Start with examples/01-basic for a gentle introduction.
Each example includes:
- Input CSS - Production-quality, well-commented CSS files
- Output Go - Pre-generated constants with rich documentation
- README - Detailed explanation of patterns and usage
See the examples README for a complete guide.
Each constant includes rich metadata as Go comments:
const BtnBrand = "btn--brand"
// @layer components
//
// **Base:** .btn
// **Context:** Use with .btn for proper styling
// **Overrides:** 2 properties (background, color)
//
// **Visual:**
// - background: `var(--ui-color-brand)` 🎨
// - color: `var(--ui-color-brand-on)` 🎨
//
// **Pseudo-states:** :hover, :focus, :activeYour IDE shows this when you hover over ui.BtnBrand, giving instant CSS context without leaving your editor.
- Errors → Exit code 1 (typos, invalid classes)
- Warnings → Exit code 0 (hardcoded strings that should use constants)
This allows gradual migration without blocking development.
cssg -lint-only
# ✓ Passes CI if no typos (warnings are informational)cssg -lint-only -strict
# ✗ Fails CI on any issue (errors OR warnings)Use strict mode once you've migrated critical templates.
cssgen supports five output formats via -output-format:
| Format | Best For... | Visual Detail |
|---|---|---|
issues |
Local development | High (Inline code pointers) |
summary |
Quick health checks | Low (Aggregated stats only) |
full |
Deep-dive audits | Maximum (Everything) |
markdown |
PR Comments / CI | Medium (Formatted for web) |
json |
Custom Tooling | Machine-readable |
Golangci-lint style - errors and warnings only:
internal/web/components/button.templ:12:8: invalid CSS class "btn--primray" (csslint)
<button class="btn btn--primray">
^
internal/web/components/card.templ:5:8: hardcoded CSS class "card" should use ui.Card constant (csslint)
<div class="card">
^
12 issues (1 errors, 11 warnings):
* csslint: 12
Hint: Run with -output-format full to see statistics and Quick Wins
Statistics and Quick Wins only (no individual issues):
CSS Constant Usage Statistics
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total constants: 232
Actually used: 8 (3.4%)
Available for migration: 95 (41.0%)
Completely unused: 129 (55.6%)
Top Migration Opportunities
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. "btn" → ui.Btn (23 occurrences)
2. "card" → ui.Card (18 occurrences)
...
Everything (issues + statistics + Quick Wins):
[All issues listed]
[Statistics summary]
[Quick Wins]
Machine-readable JSON for tooling integration:
{
"issues": [...],
"stats": {...},
"quickWins": [...]
}Shareable reports for GitHub issues, wikis, or documentation:
# CSS Linting Report
## Summary
- **Total Issues:** 225
- **Errors:** 12
- **Warnings:** 213
...# Generate constants from CSS
cssg
# Generate + lint in one pass
cssg -lint
# Lint only (no generation)
cssg -lint-only
# Quiet mode (exit code only, for pre-commit hooks)
cssg -lint-only -quiet
# Weekly adoption report
cssg -lint-only -output-format summary
# Export Markdown report
cssg -lint-only -output-format markdown > css-report.md# Custom source/output directories
cssg -source ./assets/css -output-dir ./pkg/styles -package styles
# Specific file patterns
cssg -include "components/**/*.css,utilities.css"
# Limit linting scope
cssg -lint-only -lint-paths "internal/views/**/*.templ"
# Limit output (CI performance)
cssg -lint-only -max-issues-per-linter 50name: Lint
on: [push, pull_request]
jobs:
css-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install cssg
run: go install github.com/yacobolo/cssgen/cmd/cssgen@latest
- name: Lint CSS classes
run: cssg -lint-only# Taskfile.yml
tasks:
css:gen:
desc: Generate CSS constants
cmds:
- cssg
css:lint:
desc: Lint CSS usage (fast - issues only)
cmds:
- cssg -lint-only
css:report:
desc: Weekly CSS adoption report
cmds:
- cssg -lint-only -output-format summary
check:
desc: Run all checks (Go + CSS)
cmds:
- task: test
- task: css:lint
- golangci-lint run.PHONY: css-gen css-lint check
css-gen:
cssg
css-lint:
cssg -lint-only
check: test css-lint
golangci-lint run- Scan - Find CSS files matching glob patterns
- Parse - Extract classes using native CSS parser (tdewolff/parse)
- Analyze - Detect BEM patterns, build inheritance tree
- Generate - Write Go constants with rich comments
Note: The parser supports standard CSS only. For Tailwind/PostCSS-specific syntax (like
@applyor nested selectors), ensure your build process outputs standard CSS before runningcssgen.
- Load - Parse generated
styles*.gen.gofiles to build class registry - Scan - Find all
class=attributes in.templand.gofiles - Match - Check each class against registry (with greedy token matching)
- Report - Output issues in golangci-lint format
cssgen uses pure 1:1 mapping between CSS classes and Go constants:
.btn { } → const Btn = "btn"
.btn--brand { } → const BtnBrand = "btn--brand"
.card__header { } → const CardHeader = "card__header"NOT joined constants:
// ❌ WRONG - Creates pollution and false positives
const BtnBrand = "btn btn--brand"
// ✅ CORRECT - Pure 1:1 mapping
const Btn = "btn"
const BtnBrand = "btn--brand"This ensures:
- Zero false positives - Linter suggestions are always accurate
- Composability - Mix and match any classes:
{ ui.Btn, ui.BtnBrand, ui.Disabled } - Clear intent - Each constant represents exactly one CSS class
When the linter sees class="btn btn--brand":
- Check if exact match exists for
"btn btn--brand"→ No - Split into tokens:
["btn", "btn--brand"] - Match each token:
btn→ui.Btn,btn--brand→ui.BtnBrand - Suggest:
{ ui.Btn, ui.BtnBrand }
This produces accurate, predictable suggestions.
Without flags, cssgen uses these defaults:
- Source:
web/ui/src/styles - Output:
internal/web/ui - Package:
ui - Includes:
layers/components/**/*.css,layers/utilities.css,layers/base.css - Lint paths:
internal/web/features/**/*.{templ,go}
Generation:
-source DIR- CSS source directory-output-dir DIR- Go output directory-package NAME- Go package name-include PATTERNS- Comma-separated glob patterns
Linting:
-lint- Run linter after generation-lint-only- Run linter without generation-lint-paths PATTERNS- Files to scan-strict- Exit 1 on any issue (CI mode)
Output:
-output-format MODE-issues(default),summary,full,json,markdown-quiet- Suppress all output (exit code only)-max-issues-per-linter N- Limit issues shown-color- Force color output
Run cssg -h for complete flag documentation.
cssgen works with existing CSS files and standard build tools. No runtime overhead, no new syntax to learn, works with any CSS framework.
cssgen generates constants for any CSS class, including utilities:
const FlexFill = "flex-fill"
const TextCenter = "text-center"Use them just like component classes: class={ ui.Flex, ui.FlexFill }
Generated files can be large (1000+ constants). Splitting by component improves:
- IDE performance (faster autocomplete)
- Code navigation (logical grouping)
- Readability (buttons.gen.go vs 3000-line styles.gen.go)
The generator supports two formats:
markdown(default) - Rich comments with hierarchies and diffscompact- Minimal comments for smaller files
Use -format compact for a lighter output.
The linter currently targets templ and Go files. Support for html/template could be added - contributions welcome!
MIT License - see LICENSE for details.
Issues and pull requests welcome at https://github.com/yacobolo/cssgen
Built with:
- tdewolff/parse - CSS parser
- bmatcuk/doublestar - Glob matching
- fatih/color - Terminal colors
