Gerillass: A Sass Library a Coding Agent Can Actually Use
A Sass toolkit that began as the answer to writing the same CSS for the fifth time, released in 2021 and rebuilt five years later on the module system, with a machine-readable manifest the test suite refuses to let drift.

- Gerillass
- 2026
- Sass, Open Source
Documentation drifts away from code in most projects, quietly. An agent reading stale docs writes code that does not work.
The Problem It Was Built For
Frontend work has a set of problems you solve once and then solve again on every project. Centring something. Holding an element to a ratio. A triangle. A breakpoint. A gradient over an image. None of them is hard, and that is exactly why they never get put away. Each one is quick enough to rewrite that you rewrite it, slightly differently, for years.
The cost is not the typing. It is that the fifth version is not the same as the first. A ratio box written from memory forgets that an <img> with no object-fit stretches rather than crops. A breakpoint written by hand gets its min-width and max-width a pixel apart, and two components end up disagreeing about who owns 768px. A library is how a decision gets made once instead of being remade slightly wrong each time.
So I kept one. From my early days writing HTML and CSS it lived as a folder of mixins that travelled with me between projects, and it grew the way that kind of thing grows: by hitting something in real work and putting the answer somewhere it could be reused.
Why It Went Public
I had wanted to release it for years without finding the time. When the pandemic locked everyone indoors, the time arrived. I restructured the codebase, wrote the documentation, built a site for it, and put it out in 2021.
What it had to be, for anyone other than me, was three things:
- Practical. Every mixin answers a problem that actually came up, not one that completes a matrix.
- Flexible. It adapts to the project rather than asking the project to adopt a system. No reset you must accept, no grid you must buy into, no class names in your markup.
- Quiet in the output. Mixins, not classes, so nothing ships that you did not call.
That last pair is also what separates it from a framework. Bourbon, Susy, Scut and Bootstrap all shaped it, but a framework hands you an opinion about how the whole page is built. Gerillass hands you the one thing you were about to write badly.
What Is In It
53 mixins and 23 functions, grouped by the kind of problem they close:
- Layout:
center,position,sizer,aspect-ratio,columnizer,stretched-link, andescape-to-parent, which lets a child break out of a container that is constraining it. - Responsive:
breakpointfor media queries,containerandcontainer-queryfor the component-level equivalent,adaptivefor a container that steps up at each breakpoint, plussmartphoneandtablet. - Typography:
ellipsisandline-clampfor overflow,font-face,text-gradient,text-stroke,text-shadow, andfluid, a function returning aclamp()that grows with the viewport. - Surface and shape:
circle,triangle,border-radius,scissors,linear-gradient,radial-gradient,background-dots,background-stripes,background-image. - Structure and state:
clearfix,border-box,reset-css,all-buttonsandall-text-inputsfor styling every control at once,placeholder,text-selection,hide.
Underneath sit the 23 functions the mixins are built from: unit conversion, colour shading, validation. They are public because the same arithmetic keeps coming up in the projects using the library.
Five Years On, a Different Problem
The library was fine. What had changed was who reads it.
A library with a few thousand weekly installs has essentially no training data behind it. Ask a coding agent to use Gerillass and it will not refuse. It guesses the argument forms, and Sass lets it. An unknown function is not an error in Sass: the call is emitted as literal CSS and the build passes. You get a stylesheet with remify(24px) sitting in it as text.
That failure mode is worse than a crash, because nothing surfaces it. So the fix could not be a better README. It had to be something an agent reads mechanically, and something that cannot quietly stop being true.
A Manifest That Cannot Drift
Three generated files now ship with or alongside the package:
gerillass.jsonlists every mixin and function: its signature, what each argument accepts, examples that compile, and inputs that are refused. It resolves through the package'sexportsmap, so an agent working in someone's project reads it straight out ofnode_modules/gerillass/.SKILL.mdis a written guide generated from that manifest, in the Agent Skills format, so it can be dropped into a project's skills folder.llms.txtis the same manifest shaped for the documentation site, following the llmstxt.org convention.
None of the three is written by hand. Signatures are parsed from the Sass sources; the semantics come from a separate set of notes, so nobody can describe a mixin that does not exist.
The part that actually matters is what the test suite does with them. It compiles every example the manifest contains. It takes every input the manifest claims is refused and checks that the library really refuses it, with its own error message rather than an internal Sass one. It runs every example a second time under the gls- prefixed name and requires byte-identical CSS. And it fails the build if either generated file is out of date.
So the manifest cannot claim behaviour the library does not have. That is the whole point of it: not that the docs are thorough, but that they are load-bearing. The suite is at 436 tests and 143 snapshots.
Moving to the Sass Module System
Version 2.0.0 took the library off @import and onto @use/@forward, and off the global built-ins onto namespaced ones. Dart Sass 3.0.0 removes both, so this was a deadline rather than a preference.
Two things broke, and one of them was not a style decision. All 22 utility functions carried a __ prefix, and under @use, a member whose name starts with _ is private to its own file. The prefixed names could not survive the migration at all. Worse, with @use "gerillass" as * they did not fail loudly: they compiled to literal CSS, the same silent failure an agent hits. Nineteen were a find and replace. Three needed new names, because __darken and __lighten shadow Sass built-ins silently and __null collides with a keyword.
The other break was a consolidation. ratio-box and responsive-video both held an aspect ratio with a padding-top hack, a pseudo-element and an absolutely positioned child. CSS aspect-ratio is Baseline Widely Available, which had left the two of them byte-identical to each other and wrapping barely more than one declaration. They became a single aspect-ratio mixin that closes the three gaps the bare CSS property leaves open, each measured in a browser rather than assumed: an <img> with a ratio and no object-fit is stretched rather than cropped; an <iframe> carries a 2px default border, so width: 100% overflows its container by 4px; and the ratio has to sit on the element, not on a wrapper.
Every gls- prefixed call site still works, because the prefixed half of the API is now one line: @forward "library" as gls-*. Apart from the two removals, no valid call changed its output, verified by snapshotting the CSS of every documented invocation before and after each step.
What 2.1.0 Added
Four members, chosen where the API had a hole rather than where a list looked short:
containerandcontainer-query. The responsive API wasbreakpoint, which is media queries, and component-level responsiveness had nothing at all.container-querytakes the same argument shapes asbreakpoint, so the two read alike.line-clamp. Truncating after several lines, whereellipsisdoes one. It emits five declarations because-webkit-line-clampdoes nothing on its own, and each of the four ways it silently fails was measured.fluid. A function returning aclamp()value that grows with the viewport. A function rather than a mixin because the value belongs to any property, not onlyfont-size. It keeps aremterm instead of being purevw, which is not cosmetic: under browser text zoom the rem-bearing value moved from 20.83px to 34.17px while avw-only equivalent did not respond at all. Avw-only fluid value fails WCAG 1.4.4.
Plus one accessibility fix: loadify ignored prefers-reduced-motion. Switching the animation off would have been worse than the bug: the element starts invisible and the animation is what reveals it, so the content would have stayed hidden for good. Under reduced motion the end state is now applied directly.
One Repository
The library, gerillass.com and docs.gerillass.com were three repositories on two domains, and nothing connected a mixin to the page describing it. Two things went wrong while shipping 2.1.0 because of it: llms.txt shipped pointing at a documentation page that did not exist, and eighteen function pages were reported missing twice when they had existed the whole time.
Neither was a discipline problem. Both are what happens when the only way to answer "does this member have a page?" is to fetch a website. In one repository that question is a test.
So the sites moved in. One Vite application now serves the marketing site with the documentation mounted under /docs: 80 documentation pages, one router, one build, one deploy. Every route is generated as a real HTML file at build time; the build writes 86 of them. The site is styled entirely with Gerillass, resolved through a load path to the library beside it rather than a published copy, so it is always built against the code it advertises.
Search and the Playground
The site gained a command palette over every mixin, function and page, which is what a library of 76 members needs and did not have.
The playground compiles Sass to CSS as you type, with the whole library already loaded, a version selector and a shareable URL. It is the same problem a documentation example solves, so both are built on one renderer.
The Repository as a Workspace
The work was done with Claude Code, and the repository was set up so that the next session does not have to rediscover how the project works. Four skills cover the tasks that recur: adding a member, writing a sass-true test, auditing the library, cutting a release. Each one was written because a step in it fails silently if you skip it.
Four hooks run on the repository rather than on trust: they regenerate the manifest when the sources change, check the counts the documentation claims against the counts the repository has, check that a release being prepared has its notes, and refuse a dependency change that was not asked for.
And tools/audit.js does the thing a test suite cannot. npm test only checks inputs somebody already thought of; the audit throws arguments nobody wrote a test for at all 53 mixins and all 23 functions, at every argument position, and reports what the library does with them. That is how thirteen mixins that silently emitted nothing were found.
Where It Stands
Version 2.1.0. 53 mixins, 23 functions, 436 tests, 143 snapshots. One repository holding the library, its site, its documentation and the description of itself that all three are generated from.
Five years on it is still answering the question it started with: write this once, correctly, and stop rewriting it. Now for a reader who did not exist when it was written.
Installation
npm install gerillass --save-dev
@use 'gerillass' as *;
.avatar { @include circle(50px); }
Links
Optimizing UX for VavaCars Vehicle Inspection App
Sketchize: Wireframe Templates for Designers
Working on something similar? Tell me about it.