esc
Anthology / Yagnipedia / Roll Your Own CSS

Roll Your Own CSS

The Ideal Companion to HTMX, Because Nobody Else's Opinions Fit Your Application
Principle · First observed 2025, when one developer stopped fighting frameworks and started writing CSS — though The Lizard has been doing this since 1976 without giving it a name · Severity: Liberating (the rarest severity level)

Roll Your Own CSS is the practice of writing CSS from scratch, per project, per purpose, without a framework — and discovering, after years of fighting other people’s opinions, that the cascade works perfectly when there is nothing to cascade against.

This is not a principle that anyone arrives at first. This is the principle that remains after every other principle has been tried and found to be someone else’s opinions, shipped as defaults, enforced by specificity, and removed in an afternoon when the application finally needs to look like itself instead of like every other application that used the same framework.

A developer arrived here after fighting five CSS frameworks over several years. The fights are documented in The Framework Wars and across the lifelog. The conclusion is not that frameworks are bad. The conclusion is that frameworks are other people’s applications, and other people’s applications do not know what your application is.

“I fought Pico. I fought Bulma. I fought DaisyUI. I fought Tailwind. I fought shadcn/ui. I won every fight in the same way: by removing the framework and writing CSS. The CSS I wrote was worse than the framework’s CSS. The CSS I wrote was also correct, because it described my application, not someone else’s.”
a developer, after the fifth and final framework removal

The Companion to HTMX

HTMX says: the server sends HTML. The client says “okay.”

Roll Your Own CSS says: the server sends HTML that looks right. The client says “okay” and means it.

The two are natural companions because they share the same philosophy: the browser already knows how to do this. HTML knows how to structure content. CSS knows how to style it. The browser knows how to render both. The only question is whether the HTML and CSS describe your application or someone else’s idea of an application.

HTMX eliminated the JavaScript framework by trusting the browser to render HTML. Roll Your Own CSS eliminates the CSS framework by trusting the developer to write CSS. Together, they produce a stack with no opinions to fight, no abstractions to override, and no !important declarations to confess with.

The lifelog blog — the one serving this page — is HTMX and custom CSS. Six swap targets. 651 lines of CSS. Zero frameworks. Zero build steps. The HTML arrives from the server. The CSS makes it look right. The browser says “okay.” The interaction is complete. Nobody reconciled a virtual DOM. Nobody merged utility classes. Nobody copied 200 lines of React from a component bazaar. The server sent HTML. The HTML had classes. The classes had styles. The styles were written by the person who built the application, for the application, because who else would know what .storyline-shelf should look like?

The Mirror

The argument for Roll Your Own CSS is not performance, not bundle size, not build times — though all of these improve. The argument is the mirror.

When you write your own CSS, the class names describe your application:

.storyline-shelf { }
.episode-card { }
.wiki-infobox { }
.metric-badge { }
.pipeline-stage { }
.agent-commit-log { }

When you use a framework, the class names describe the framework:

<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">

The first list tells you what the application is. The second tells you what the application looks like. Both are useful information. But when you open a template at 2 AM because a customer reported that the pipeline stage card is rendering wrong, you want to search for .pipeline-stage, not for the specific combination of twelve Tailwind utilities that a previous developer chose to make something that looks like a pipeline stage card but is described as a flexbox container with centered items, justified spacing, one rem of padding, a white background, rounded corners, and a medium shadow.

The application and its CSS can see each other in the mirror. The class .pipeline-stage in the template maps to .pipeline-stage in the stylesheet. The template says what the thing is. The stylesheet says what the thing looks like. They agree. They have always agreed. They will continue to agree because they were written by the same person for the same purpose and there is no framework between them interpreting, overriding, or cascading someone else’s opinions into the gap.

“The class name should describe the thing. Not the layout. Not the spacing. Not the color. The thing. The thing knows what it is. The CSS knows what it looks like. The developer knows both because the developer wrote both.”
The Lizard, on naming

The Grief

CSS frameworks introduce grief. Not all at once. Gradually, compounding, the way technical debt always accumulates — not through bad decisions but through reasonable decisions that were reasonable for someone else’s application.

The grief has a pattern. riclib documented it across five frameworks:

Pico CSS: The classless framework styled everything by default. Beautiful for prototypes. Then the application needed a sidebar Pico didn’t anticipate. Inline overrides accumulated. 347 inline styles. !important declarations fighting each other. Eighteen months of grief. Removed in one afternoon. Nine commits.

Bulma: Component classes with grid opinions. The opinions were good opinions. They were not the application’s opinions. The grid needed to be different. The components needed to be different. The overrides began. The grief began.

DaisyUI: Tailwind with pre-built components. Two layers of abstraction. When something broke, the developer debugged DaisyUI’s interpretation of Tailwind’s interpretation of what the developer wanted. Debugging two layers of someone else’s opinions is not debugging. It is archaeology.

Tailwind CSS: Maximum flexibility. Zero opinions about components. But a vocabulary tax — learning the abbreviated class names, memorizing the spacing scale, reading class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md" and parsing it back into “a card.” The HTML became unreadable. The CSS did not exist. The grief was in the reading.

shadcn/ui: Copy-paste components. “You own the code.” You own 200 lines of React, Tailwind, and Radix primitives per component. You own the maintenance. You own the update process. You own someone else’s code, which is the specific form of ownership that feels like freedom and functions like debt.

Each framework removed a specific kind of grief and introduced a different kind. The net grief was conserved. This is Liberato’s Grief Conservation Law, an extension of Liberato’s Law: every CSS framework conserves the total grief by transforming it from one form to another. The only way to reduce total grief is to have less CSS that fights less CSS, which means writing your own.

The Fix

When something is broken in a CSS framework, the developer must:

  1. Understand what the framework is doing (read the source)
  2. Understand why the framework’s opinion differs from the application’s need (read the docs)
  3. Find the correct override mechanism (specificity? custom property? plugin? configuration?)
  4. Apply the override without breaking other things the framework is doing (prayer)
  5. Test that the override survives framework updates (more prayer)

When something is broken in your own CSS, the developer must:

  1. Open the file
  2. Fix it

This is not a trivial difference. This is the difference between negotiating with someone else’s code and editing your own. The first requires understanding two systems — the application and the framework. The second requires understanding one. The cognitive load is halved. The fix is immediate. The !important is unnecessary because there is no conflicting opinion to override. The cascade cascades from your intentions to the screen, with nothing in between.

“I have been fixing my own CSS since 1976. The process is: I change the value. The value changes. I have never needed to override a framework’s opinion because I have never had a framework. The absence of a framework is not a limitation. It is the fix.”
The Lizard, whose CSS is printf formatting green text on a black background

The Numbers

The lifelog blog: 651 lines of CSS. One Go function. go:embed. Seven storyline themes with distinct accent colors. Responsive layout. Dark mode. Wikipedia-style infoboxes for Yagnipedia. Lightbox for cover images. !important declarations: zero.

The V4 product: 13,000 lines across 34 CSS files. Its own design system. tokens.css for design tokens — colors, spacing, typography, shadows, breakpoints. BEM-inspired naming. Classes like .pipeline-card, .metric-badge, .agent-log-entry. Inspired by shadcn/ui’s aesthetics — the visual quality, the spacing, the typography choices — but semantic to the application’s domain. go:embed as the build step. No PostCSS. No Tailwind config. No bundler. !important declarations: zero.

Both projects were written by one developer. Both projects are maintained by one developer. Both projects can be understood by reading the CSS, because the CSS describes the application, and the application is what the developer is thinking about at 2 AM when the customer reports that the pipeline stage card is rendering wrong.

The Pattern

Roll Your Own CSS works when:

  1. One developer (or a small, aligned team) writes both the HTML and the CSS. The class names emerge from the domain because the developer knows the domain. No translation layer needed.

  2. The application has a specific identity. If every application looks the same (Bootstrap era), frameworks make sense. If the application needs to look like itself, no framework knows what “itself” looks like.

  3. HTMX (or server-rendered HTML) is the delivery mechanism. When the server sends complete HTML, the CSS can be complete too. No component hydration. No CSS-in-JS runtime. No style injection. Just a <link> tag and a file.

  4. The developer is willing to learn CSS. This is the prerequisite that every framework exists to avoid, and that every framework eventually requires anyway, because overriding a framework requires knowing CSS better than writing CSS from scratch does.

Roll Your Own CSS does not work when the team is fifty people, the design system needs to enforce consistency across twelve teams, and the class names need to be a shared vocabulary that outlasts any individual developer. In that case, use Tailwind, use shadcn, use Bootstrap — use the framework that keeps fifty people aligned. The framework’s opinions are the alignment mechanism. One person does not need an alignment mechanism. One person is already aligned with themselves.

“The Squirrel proposed a design system with tokens, scales, variants, responsive breakpoints, dark mode toggles, and a theme generator. The developer opened a file and typed .card { padding: 1rem; }. The card has padding. The card has always needed padding. The padding did not need a system.”
The Passing AI, on the distance between design systems and CSS

The Shimmer Test

On March 12, 2026, a developer and an AI pair-programmed a feature: when the agent waits for a long tool call, the blinking cursor should fade into a rotating fun word — Pondering…, Ruminating…, Crafting… — with a CRT-style shimmer that sweeps an accent-colored highlight across the italic text.

The entire CSS:

.thinking-word {
  font-size: var(--text-sm);
  font-style: italic;
  background: linear-gradient(
    90deg,
    var(--text-muted) 0%,
    var(--text-muted) 40%,
    var(--accent-primary) 50%,
    var(--text-muted) 60%,
    var(--text-muted) 100%
  );
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: thinking-word-fade 0.4s ease-out,
             thinking-shimmer 3s ease-in-out infinite;
}

@keyframes thinking-shimmer {
  0% { background-position: 200% center; }
  100% { background-position: -200% center; }
}

Ten lines of properties. Two keyframes. One class name that describes what the thing is. A gradient slides across text. The developer said “gorgeous.” The AI committed. Total elapsed time from idea to commit: four minutes. Total !important declarations: zero. Total build steps: zero. Total framework documentation consulted: zero.

Now consider the alternatives.

In Tailwind, you would write:

<span class="text-sm italic bg-gradient-to-r from-gray-400 via-indigo-500
  to-gray-400 bg-[length:200%_100%] bg-clip-text text-transparent
  animate-[shimmer_3s_ease-in-out_infinite]">
  Pondering...
</span>

Except animate-[shimmer_3s_ease-in-out_infinite] requires a custom keyframe, which means opening tailwind.config.js and adding:

module.exports = {
  theme: {
    extend: {
      keyframes: {
        shimmer: {
          '0%': { backgroundPosition: '200% center' },
          '100%': { backgroundPosition: '-200% center' },
        },
      },
      animation: {
        shimmer: 'shimmer 3s ease-in-out infinite',
      },
    },
  },
}

And then also bg-clip-text doesn’t exist in Tailwind’s default utilities, so you need a plugin or @apply or an arbitrary value [background-clip:text]. And then -webkit-background-clip needs the vendor prefix, which Tailwind handles through PostCSS, which means you need a build step, which means you need a bundler, which means the four-minute shimmer has become a twenty-minute yak shave through configuration files that describe the build system, not the application.

The class name in Tailwind tells you: this is small italic text with a gradient background clipped to text, transparent, with a custom animation. The class name .thinking-word tells you: this is a thinking word. One of these you can search for at 2 AM. The other one you can gaze at and wonder which of the fourteen utility classes is the one making the shimmer start from the wrong side.

In Bootstrap, you would discover that Bootstrap does not have a gradient text animation utility. You would write custom CSS. You would write the same ten lines. You would also fight Bootstrap’s $font-size-sm variable, Bootstrap’s $text-muted color, and Bootstrap’s opinion about what italic means in the context of a design system that was designed for Twitter’s admin panel in 2011. You would write the CSS anyway and add !important to three of the ten lines because Bootstrap’s specificity is higher than yours. The shimmer would work. The !important declarations would haunt you.

In shadcn/ui, you would install Radix primitives, create a ThinkingWord component in React, import cn() from your utils, compose twelve Tailwind classes through cva() variants, realize you still need the custom keyframe in the Tailwind config, add the config, run the build, discover the animation doesn’t work because Radix’s motion prop conflicts with your custom animation, search GitHub issues for forty minutes, find a workaround involving forwardRef and useEffect, implement the workaround, and ship a component that does what ten lines of CSS did — but componentized, which means reusable, which means you can now shimmer text in twelve places across your application, which you will never do because this is a thinking indicator and there is exactly one thinking indicator.

In the developer’s own CSS, the developer typed ten lines. The text shimmered. The developer said “gorgeous.” The AI committed.

“The shimmer is a gradient on text. The gradient is three colors. The animation moves the gradient. This is not a component. This is not a system. This is not a configuration. This is CSS doing what CSS does. The browser knows how to animate a gradient. The developer knows what a thinking word looks like. Nobody else needed to be involved.”
The Lizard, who has never needed a shimmer because green text on black is already perfect

Measured Characteristics

See Also