Fixing a Broken CSS Header System
When a typography system breaks, it’s rarely just one thing. Today’s forensic audit of our CSS header system revealed a cascade of failures: missing base styles, conflicting systems, and a complete breakdown of typographic hierarchy. Here’s what we found and how we fixed it.
The Initial Symptoms
Users reported that headers appeared “as small as body text” and lacked any visual hierarchy. The Newsreader font—a beautiful variable serif designed for editorial elegance—was nowhere to be seen on most headers. Dark mode made things worse, with inconsistent weights and poor contrast.
Phase 1: Technical Error Identification
Critical Error #1: The Empty Foundation
The most shocking discovery came in tailwind.config.ts
:
addBase({}); // 🚨 EMPTY!
This single line meant zero base styles for any HTML headers. Raw <h1>
through <h6>
elements had:
- No font sizes defined
- No font family assigned
- No weights specified
- No spacing rules
Headers literally inherited body text sizing (15-17px), explaining why they appeared invisible in the hierarchy.
Critical Error #2: Three Conflicting Systems
We discovered three separate, uncoordinated header systems:
- Semantic Classes (
.heading-1
through.heading-6
)
- Defined in Tailwind components
- Used fluid type scale
- Applied Newsreader font
- Prose Typography Plugin
- Defined styles within
.prose
wrapper - Attempted to use non-existent theme values
- Different weight system
- Base Styles
- Missing entirely
- Headers fell back to browser defaults
High Priority Error: Fluid Type Integration Failure
The tailwindcss-fluid-type
plugin generates utility classes but doesn’t populate theme values:
// This FAILS - no theme('fontSize.4') exists!
h1: {
fontSize: theme("fontSize.4"), // ❌ undefined
}
The plugin creates classes like text-4
but theme('fontSize.4')
returns nothing. This caused prose headers to break completely.
Phase 2: Typographic Hierarchy Assessment
Visual Hierarchy Failures
Analyzing the intended scale revealed good mathematical progression but poor implementation:
Level | Mobile → Desktop | Actual Weight | Issue |
---|---|---|---|
H1 | 37px → 52px | 400 | Too light for impact |
H2 | 29px → 41px | 425 | Barely distinguishable |
H3 | 23px → 33px | 450 | Acceptable |
H4 | 18px → 26px | 500 | Good |
H5 | 15px → 21px | 550 | Heavier than H4! |
H6 | undefined | undefined | Missing entirely |
The weight progression was inverted—smaller headers appeared heavier than larger ones.
Dark Mode Chaos
Different systems defined different dark mode weights:
- Semantic classes: 450-550 range
- Prose typography: 425-550 range
- Base styles: none (fell back to light mode)
Phase 3: Newsreader-Specific Failures
Optical Size Misuse
Newsreader is a variable font with an optical size axis (opsz
). The implementation set static values:
h1 { font-variation-settings: "opsz" 72; } /* For 52px text */
But on mobile, H1 renders at 37px—requiring opsz
48 for optimal legibility. The optical sizes were never adjusted for viewport changes.
Underutilized Weight Range
Newsreader supports weights 200-800, but we only used 400-550, missing opportunities for:
- Stronger contrast between levels
- Better dark mode optimization
- More expressive headlines
The Solution: A Unified System
Step 1: Establish Base Styles
First, we populated the empty addBase()
:
addBase({
'h1, h2, h3, h4, h5, h6': {
fontFamily: theme('fontFamily.headline'),
fontWeight: '400',
lineHeight: '1.2',
letterSpacing: '-0.01em',
color: theme('colors.accent-base'),
fontFeatureSettings: '"kern" 1, "liga" 1, "calt" 1',
},
h1: {
fontSize: 'var(--step-5)',
fontWeight: '450',
fontVariationSettings: '"opsz" 72, "wght" 450',
letterSpacing: '-0.025em',
},
//... h2 through h6
});
Step 2: Define Fluid Type Variables
Added CSS custom properties for the fluid scale:
:root {--step--2: clamp(0.64rem, 0.62rem + 0.09vw, 0.72rem);--step--1: clamp(0.80rem, 0.77rem + 0.14vw, 0.90rem);--step-0: clamp(0.94rem, 0.89rem + 0.23vw, 1.06rem);--step-1: clamp(1.17rem, 1.10rem + 0.37vw, 1.33rem);--step-2: clamp(1.46rem, 1.36rem + 0.55vw, 1.66rem);--step-3: clamp(1.83rem, 1.67rem + 0.79vw, 2.08rem);--step-4: clamp(2.29rem, 2.07rem + 1.11vw, 2.59rem);--step-5: clamp(2.86rem, 2.55rem + 1.55vw, 3.24rem);--step-6: clamp(3.58rem, 3.14rem + 2.17vw, 4.05rem);
}
Step 3: Responsive Optical Sizing
Implemented viewport-aware optical sizes:
@media (max-width: 640px) {
h1 { font-variation-settings: "opsz" 48, "wght" 450; }
h2 { font-variation-settings: "opsz" 36, "wght" 475; }
h3 { font-variation-settings: "opsz" 28, "wght" 500; }
/*... */
}
Step 4: Fix Weight Progression
Established a logical weight hierarchy:
Level | Weight | Rationale |
---|---|---|
H1 | 450 | Strong but elegant |
H2 | 475 | Slightly heavier for clarity |
H3 | 500 | Clear mid-level weight |
H4 | 525 | Noticeable increase |
H5 | 550 | Substantial for small size |
H6 | 600 | Bold for tiny uppercase |
The Results
Before
- Headers indistinguishable from body text
- No Newsreader font on raw headers
- Broken hierarchy with inverted weights
- Poor mobile rendering
After
- Clear 25% size progression between levels
- Newsreader applied consistently
- Logical weight progression
- Responsive optical sizing
- Proper dark mode optimization
Testing Checklist
✓ Raw headers display correct sizes ✓ Semantic classes enhance headers properly ✓ Prose headers maintain hierarchy ✓ Dark mode weights render correctly ✓ Mobile optical sizes adjust ✓ Newsreader font applies to all headers ✓ Weight progression creates clear hierarchy ✓ Multi-line headers wrap properly ✓ Accessibility contrast meets WCAG AA ✓ Performance: no layout shift
Key Lessons
- Never leave
addBase()
empty - Base styles are the foundation everything builds upon - Coordinate typography systems - Multiple systems must work in harmony, not competition
- Understand plugin behavior - Fluid type plugins may not populate theme values
- Use variable font features - Optical sizing dramatically improves readability
- Test the full cascade - Check raw elements, utility classes, and component styles
Final Typography Specification
The repaired system now provides:
- H1: 45-52px, weight 450, optical size 48-72
- H2: 37-41px, weight 475, optical size 36-52
- H3: 29-33px, weight 500, optical size 28-36
- H4: 23-26px, weight 525, optical size 24-28
- H5: 19-21px, weight 550, optical size 20-24
- H6: 15-17px, weight 600, optical size 18-20, uppercase
Each level is visually distinct, semantically appropriate, and optimized for its size range. The Newsreader font now shines at every level, providing the editorial elegance it was designed for.
Conclusion
A broken typography system is like a house with no foundation—everything above it becomes unstable. By systematically identifying each failure point and building a coordinated solution, we transformed chaos into clarity. The header system now provides the solid typographic hierarchy every design system needs.
Remember: typography isn’t just about making text look good. It’s about creating a systematic, maintainable, and accessible foundation for communication. When that foundation cracks, everything built upon it suffers. But with careful analysis and thoughtful implementation, even the most broken system can be restored to excellence.