LASS: CSS in Common Lisp
Mistakes I Made While Converting My Blog Stylesheet to LASS
Introduction
Over the last few months I have been slowly moving parts of my personal infrastructure replacing Python and others toward a stack that is mostly built around Common Lisp, with Emacs and. It is not a dogmatic goal, who am I kidding I am becoming dogmatic. I wrote it down on my college notebook a decade and half ago, that I’d convert all my favorite tools and implement them in C.
I have been on and off with Common lisp in these years, but never got around to actually using it for any useful purpose and I regret it. I am familiar with the language, but last few weeks I am implementing simple utilities that become part of my personal computing.
One of the projects I built recently was a small Common Lisp service for capturing links and archiving them. That project which I call link-capture forced me to learn about HTTP servers, small deployment scripts, and a few Common Lisp libraries. Once that project stabilized, I wanted another small project that would exercise different parts of the ecosystem.
The best way I know to learn a language or a new toolchain is not by reading about it, but by building things that are real enough to matter, yet not serious enough to cause damage if they break. Blog infrastructure is perfect for that.
So I decided to convert my blog stylesheet from plain CSS into LASS, a CSS DSL written in Common Lisp. Author of LASS library is the infamous Shinmera who is a venerable Common Lisper. This document is both a blog post and a technical note to my future self. When I inevitably run into similar problems again, I want to remember exactly what went wrong, what I tried, and how I fixed it.
Please note that this stylesheet conversion was not about performance or necessity. It was part of a larger effort to build familiarity with the Common Lisp ecosystem. My working stack for most personal projects now tends to look like this:
- Python for data processing and small utilities
- Emacs for editing and my main interface to all of my tools
- Common Lisp for services and tooling (hopefully completely switch to)
There is something about the simplicity of S-expressions. But the goal at this point is not purity and goal is fluency. Every time I build something small in Lisp even something as mundane as compiling CSS I get more comfortable with the language and its libraries. And my blog infrastructure is an ideal playground. It is real work, but the consequences of failure are minimal.
Nothing here is theoretical. Every section corresponds to a real error I encountered while doing the conversion.
Why LASS?
LASS is a library that lets you express CSS as Lisp data structures. Instead of writing CSS directly like this:
h1, h2, h3 { font-weight: bold; }
you write:
((:or h1 h2 h3) :font-weight bold)
and LASS compiles that structure into CSS.
The important thing is that LASS is not a macro language like Sass or Less. It is simply a DSL expressed as Lisp data. That distinction turns out to matter a lot. Because the moment you forget that CSS is now data structures, the LASS compiler reminds you very quickly.
Installing LASS and using it
For some reason, I couldn’t install and load LASS using quicklisp as usual (ql:quickload :lass). So I cloned the repo and into local directory.
$ cd ~/.roswell/lisp/quicklisp/local-projects/
$ git clone https://codeberg.org/shinmera/LASS.git
Cloning into 'LASS'...
remote: Enumerating objects: 423, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 423 (delta 0), reused 0 (delta 0), pack-reused 420 (from 1)
Receiving objects: 100% (423/423), 146.84 KiB | 135.00 KiB/s, done.
Resolving deltas: 100% (247/247), done.
Here is the build script that I use to compile my .lass files into .css files.
(declaim (optimize (debug 3) (safety 3))) (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :lass)) (handler-case (progn (flet ((compile1 (in out) (uiop:ensure-all-directories-exist (list out)) (with-open-file (s in) (lass:generate s :out out :pretty t)))) (compile1 #P"cl/lass/style.lass" #P"static/css/style.css") (compile1 #P"cl/lass/dark.lass" #P"static/css/dark.css") (compile1 #P"cl/lass/white_clean.lass" #P"static/css/white_clean.css") (compile1 #P"cl/lass/light.lass" #P"static/css/light.css")) (uiop:quit 0)) (error (e) (format *error-output* "~&LASS build error: ~A~%" e) (uiop:quit 2)))
Mistakes
Very first mistake
(lass:compile-and-write '(:let ((btn-pad "0.35em 0.8em") (btn-br "6px") (btn-bdr "#ccc") (btn-bg "#f7f7f7") (btn-fg "#333")) (body :font-family Calibri :line-height 1.2 :margin "auto" (:media "(min-width: 768px)" (body :line-height 1.75)))))
The value "(min-width: 768px)" is not of type LIST [Condition of type TYPE-ERROR] Restarts: 0: [RETRY] Retry SLY evaluation request. 1: [*ABORT] Return to SLY's top level. 2: [ABORT] abort thread (#<THREAD tid=3654473 "slynk-worker" RUNNING {12028EE0B3}>)
I was wrecking my head with this, and finally posted a question to the codeberg repo. Evidently “A media query cannot be part of another block”. Anyway my first attempt was naive. I essentially copied the CSS rules and wrapped them in parentheses.
(selector :property value)
This technically worked in some places, but it preserved all the structural problems from the original CSS.
Misunderstanding Nested Selector Structure
One of the first real errors I encountered looked like this. The error messages weren’t that helpful to my eyes. I thought this would work, but LASS interprets li as if it were a property name. Since it is not a keyword, the compiler fails. The correct structure is:
(lass:compile-and-write '(.post :position relative li :list-style none))
.post{ position: relative li; list-style: none; }
For the following, the resulting CSS is fine
(lass:compile-and-write
'(.post
(li :list-style none)))
.post li{ list-style: none; }
Selectors must always appear as nested lists. This error happened multiple times throughout the conversion. There were layers of indirections and I didn’t know where the problem was. For instance this is in the following my build script I was using this function wrong like this (uiop:ensure-all-directories-exist out)
Incorrect Pseudo-Selector Nesting
Another early mistake produced this output. That space between b and :before is catastrophic. It changes the meaning of the selector entirely. The mistake looked like this:
(lass:compile-and-write '((:or b strong) ((:or :before :after) :content "*")))
b :before, b :after, strong :before, strong :after{ content: "*"; }
Lets take a simpler case to see this clearly
(lass:compile-and-write '((b :before) :margin 0))
b :before{ margin: 0; }
The correct form requires :and:
(lass:compile-and-write '((:and (:or b strong) (:or :before :after)) :content "*"))
b:before, b:after, strong:before, strong:after{ content: "*"; }
LASS treats selectors as composable structures. If they are not explicitly fused with :and, they become separate segments.
One other place I missed :and
This is also where the unwanted space created issues.
(lass:compile-and-write '(p (:last-child :margin-bottom 0)))
p :last-child{ margin-bottom: 0; }
Then I tried this without much thought:
(p:last-child :margin-bottom 0)
Neither worked. The correct form was:
(lass:compile-and-write '((:and p :last-child) :margin-bottom 0))
p:last-child{ margin-bottom: 0; }
Again, pseudo-classes must be structurally combined with selectors.
Using Lists Where LASS Expected CSS Values
At one point I attempted to construct CSS values using Lisp lists.
(lass:compile-and-write
'(.post
:margin (list 0 0 20px)))
Don't know what to do with 0 (not part of a property).
[Condition of type SIMPLE-ERROR]
Restarts:
0: [RETRY] Retry SLY evaluation request.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD tid=3719551 "slynk-worker" RUNNING {12033463E3}>)
LASS does not interpret arbitrary Lisp lists as CSS value groups. The correct form is simply:
(lass:compile-and-write
'(.post
:margin 0 0 20px))
.post{ margin: 0 0 20px; }
or using variables.
Selector Quoting Issues
Selectors such as caused problems because the Lisp reader treats # specially. This fails:
(lass:compile-and-write
'(#table-of-contents
:margin 10px))
no dispatch function defined for #\t Stream: #<dynamic-extent STRING-INPUT-STREAM (unavailable) from "(let ((*..."> [Condition of type SB-INT:SIMPLE-READER-ERROR]
We have to quote them in | for successful compilation. Normal quotes " work too.
(lass:compile-and-write
'(|#table-of-contents|
:margin 10px))
#table-of-contents{ margin: 10px; }
Quoting selectors becomes necessary whenever the Lisp reader might reinterpret them.
Attempting Arithmetic Inside CSS Values
Eventually I moved these into a :let block at the top of the stylesheet. Centralizing layout constants made the file much easier to maintain. Another experiment involved spacing calculations, but it is failed. I am pretty sure, I can
(lass:compile-and-write '(:let ((sp3 10) (base-px 5) (margin1 (* sp3 base-px))) (.post :margin #(margin1))))
.post{ margin: *(SP3,BASE-PX); }
The resulting CSS literally contained *(SP3,BASE-PX). The lesson here is subtle. LASS does not evaluate arbitrary Lisp arithmetic inside CSS expressions. Values must already resolve to CSS tokens. In other words, computation must happen before the rule is emitted.
How LASS Thinks About CSS
LASS treats CSS selectors as data structures rather than strings. Selector sets can be expressed using operators such as :or.
((:or h1 h2 h3) :font-weight bold)
Selector combinations use :and.
((:and code :before) :content "=")
More complex expressions generate cartesian selector expansions.
((:and (:or b strong) (:or :before :after)) :content "*")
Which expands into four selectors automatically. Nesting also becomes structural rather than textual.
(.post (li :list-style none) (img :max-width 100%))
The compiler propagates the parent selector automatically.
When LASS Is Worth Using (and When It Isn’t)
For very small stylesheets, plain CSS is perfectly adequate. Introducing a DSL may add unnecessary complexity. However, when stylesheets begin to exhibit repeated structural patterns, LASS becomes useful. It allows the stylesheet to be expressed in terms of relationships rather than duplicated text. Selector families, nested components, and centralized constants all become easier to manage. For projects already built around Lisp, this style of expression feels natural.
Summary of Mistakes
| # | Category | Mistake | Symptom | Lesson |
|---|---|---|---|---|
| 1 | Conceptual | Treated LASS like CSS syntax | Structural duplication | CSS must be treated as data |
| 2 | Selector structure | Bare selector inside rule | “Don’t know what to do with LI” | Selectors must be nested |
| 3 | Pseudo-elements | Incorrect nesting | b :before output |
Use :and |
| 4 | Pseudo-classes | Wrong syntax for :last-child |
Invalid selectors | Combine with :and |
| 5 | CSS values | Used Lisp lists | “Don’t know what to do with 0” | Use inline CSS values |
| 6 | Arithmetic | Tried runtime multiplication | Broken CSS output | Precompute values |
| 7 | Reader syntax | Unquoted #table-of-contents |
Reader confusion | Quote selectors |
| 8 | Properties | Missing keyword prefix | Invalid property forms | Use Lisp keywords |
| 9 | Conversion errors | Missing rules | Layout breakage | Verify translation |
| 10 | Refactoring timing | Refactored too early | Hard debugging | Stabilize first |
Conclusion
Converting my stylesheet to LASS did not dramatically change the CSS delivered to the browser. What it changed was the way the stylesheet is expressed and reasoned about. Attempting structural compression too early was the biggest mistake of all. At one point I started aggressively restructuring selectors before the syntax issues were resolved. That was a mistake. Every time the compiler threw an error, it became harder to identify the root cause. The correct workflow turned out to be: (i) Get the translation working. (ii) Stabilize the syntax. (iii) Then refactor the structure. Selectors became composable structures. Repeated patterns became explicit. Constants could be centralized. More importantly, the process served its real purpose: learning the ecosystem by building small, real projects. And now, when I encounter the same errors again in the future, I will have a detailed record of how I solved them.