Deep Dive: How Tailwind CSS Came to Be + What We Can Learn
After experimenting with some prototypes (mercss.zig, streaming_css.zig), here are my thoughts on streaming + CSS evolution:
The Tailwind Success Story
Problem Tailwind solved:
- CSS specificity wars (
!important hell)
- Naming fatigue (
.btn-wrapper-inner-left)
- Giant CSS files (10KB+ of unused styles)
- Context switching (HTML → CSS file → back)
Their solution:
- Utility-first:
flex pt-4 text-center bg-blue-500
- Atomic classes: Each does one thing
- Composability: Classes compose like LEGO
- Build-time: JIT compiler generates only used classes
- Constraints: Design tokens (colors, spacing) enforced
Why it took off:
- No naming = move faster
- Small bundles (purged automatically)
- Design system by default
- "Just write HTML"
Where Tailwind Has Pain Points
- Runtime build pipeline: PostCSS → JIT compiler → PurgeCSS
- Scanning: Must scan all source files to find classes
- Config drift:
tailwind.config.js separate from code
- Dynamic values:
w-[123px] is runtime-evaluated
- No type safety: Wrong color name = runtime error
The Zig/merjs Opportunity
What Zig's comptime gives us that Tailwind can't do:
1. Compile-Time CSS Generation
// merjs knows ALL components at compile time
// No scanning needed!
const button = mer.css(.{
.base = .{
.padding = .{.x = .md, .y = .sm},
.background = .primary, // Type-safe design token
},
.states = .{
.hover = .{ .background = .primary_light },
},
});
Generates at comptime:
.mcss-a7f3e { padding: 8px 16px; background: #3b82f6; }
.mcss-a7f3e:hover { background: #60a5fa; }
No build step. No purging. No scanning. Only used styles exist.
2. Streaming-Aware CSS
Current problem:
- HTML streams first → browser renders with no styles
- CSS arrives later → flash of unstyled content
Solution:
// CSS streams WITH the component
return mer.html(
h.div(.{ .class = styles.card }, .{
// Critical CSS sent in shell
h.header(.{}, "Card Title"),
// Async component brings its own CSS
h.Suspense(.{
.fallback = h.spinner(),
.css = .lazy, // Only load if component renders
}, asyncData()),
})
);
CSS arrives in chunks:
<style>/* critical: layout, header */</style>
<div class="card">
<header>Card Title</header>
<style>/* component: spinner animation */</style>
<div class="spinner">...</div>
<!-- Later, when data arrives -->
<style>/* component: table styles */</style>
<table>...</table>
</div>
3. Type-Safe Design Tokens
// Define design system in Zig
const Theme = struct {
colors = .{
.primary = "#3b82f6",
.danger = "#ef4444",
},
spacing = .{ .sm = 4, .md = 8, .lg = 16 },
};
// Compile-time error if wrong token
.button = .{
.background = .primary, // ✓ OK
.padding = .xl, // ✓ OK
.color = .unknown, // ✗ Compile error!
};
4. Layout-Aware CSS
// merjs knows the DOM structure at compile time
const Layout = struct {
header: .{ .height = 64 },
sidebar: .{ .width = 240 },
main: .{ .fill_remaining = true },
};
// Generates optimal CSS without media queries
// merjs knows exact layout, generates grid/flex accordingly
5. State Machine Styling
const Button = mer.css.stateful(.{
.states = .{ .idle, .hover, .loading, .disabled },
.transitions = .{
.{ .from = .idle, .to = .hover, .duration = 200 },
.{ .from = .idle, .to = .loading, .duration = 0 },
},
.styles = .{
.idle = .{ .opacity = 1 },
.loading = .{ .opacity = 0.5, .cursor = .wait },
},
});
Generates CSS + minimal JS for state transitions. No hydration needed for styling!
The Streaming Vision
Current SSR:
Server: [HTML chunk 1] [HTML chunk 2] [HTML chunk 3]
Client: [render] [FOUC!] [hydrate] [interactive]
↑ CSS arrives separately
merjs Streaming 2.0:
Server: [shell+CSS] [component1+CSS] [component2+CSS] [hydration]
Client: [render] [render styled] [render styled] [interactive]
↑ Never unstyled!
Key innovation: CSS is part of the component chunk, not a separate resource.
Experiment Prototypes
I pushed two experiment files:
src/mercss.zig - Compile-time atomic CSS
src/streaming_css.zig - CSS that streams with components
Both are rough sketches showing the direction.
Questions for Discussion
- Atomic vs semantic: Do we want Tailwind-style
flex pt-4 or semantic .card .button?
- Inline vs extracted: CSS inline per component or extract to
<style> blocks?
- Critical CSS: How to determine what's "critical" vs "lazy"?
- State management: Should CSS handle states (hover, loading) or JS?
- Tailwind compat: Interop with existing Tailwind classes or clean break?
Wild Ideas to Consider
- Content-aware styles: CSS that adapts based on content length, image sizes, etc.
- Network-aware streaming: Send simpler CSS on slow connections, richer on fast
- Animation co-streaming: CSS animations that sync with data arrival
- Theme streaming: Dark/light mode CSS streams conditionally
- Layout transitions: Animate between SSR layout and hydrated layout
My take: The combination of Zig's comptime + streaming SSR gives us a unique opportunity to solve CSS in a way no JS framework can. We can go beyond Tailwind's utility-first to "compiler-first" - where the CSS is as optimized as the machine code.
Thoughts? Which directions feel worth exploring?
Deep Dive: How Tailwind CSS Came to Be + What We Can Learn
After experimenting with some prototypes (
mercss.zig,streaming_css.zig), here are my thoughts on streaming + CSS evolution:The Tailwind Success Story
Problem Tailwind solved:
!importanthell).btn-wrapper-inner-left)Their solution:
flex pt-4 text-center bg-blue-500Why it took off:
Where Tailwind Has Pain Points
tailwind.config.jsseparate from codew-[123px]is runtime-evaluatedThe Zig/merjs Opportunity
What Zig's comptime gives us that Tailwind can't do:
1. Compile-Time CSS Generation
Generates at comptime:
No build step. No purging. No scanning. Only used styles exist.
2. Streaming-Aware CSS
Current problem:
Solution:
CSS arrives in chunks:
3. Type-Safe Design Tokens
4. Layout-Aware CSS
5. State Machine Styling
Generates CSS + minimal JS for state transitions. No hydration needed for styling!
The Streaming Vision
Current SSR:
merjs Streaming 2.0:
Key innovation: CSS is part of the component chunk, not a separate resource.
Experiment Prototypes
I pushed two experiment files:
src/mercss.zig- Compile-time atomic CSSsrc/streaming_css.zig- CSS that streams with componentsBoth are rough sketches showing the direction.
Questions for Discussion
flex pt-4or semantic.card .button?<style>blocks?Wild Ideas to Consider
My take: The combination of Zig's comptime + streaming SSR gives us a unique opportunity to solve CSS in a way no JS framework can. We can go beyond Tailwind's utility-first to "compiler-first" - where the CSS is as optimized as the machine code.
Thoughts? Which directions feel worth exploring?