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:

  1. Semantic Classes (.heading-1 through .heading-6)
  • Defined in Tailwind components
  • Used fluid type scale
  • Applied Newsreader font
  1. Prose Typography Plugin
  • Defined styles within .prose wrapper
  • Attempted to use non-existent theme values
  • Different weight system
  1. 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:

LevelMobile → DesktopActual WeightIssue
H137px → 52px400Too light for impact
H229px → 41px425Barely distinguishable
H323px → 33px450Acceptable
H418px → 26px500Good
H515px → 21px550Heavier than H4!
H6undefinedundefinedMissing 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:

LevelWeightRationale
H1450Strong but elegant
H2475Slightly heavier for clarity
H3500Clear mid-level weight
H4525Noticeable increase
H5550Substantial for small size
H6600Bold 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

  1. Never leave addBase() empty - Base styles are the foundation everything builds upon
  2. Coordinate typography systems - Multiple systems must work in harmony, not competition
  3. Understand plugin behavior - Fluid type plugins may not populate theme values
  4. Use variable font features - Optical sizing dramatically improves readability
  5. 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.