Hi, I'm building Loom and with that I'm trying to bring better guarantees, contracts and types to CSS stylesheets.
Look, most frontend teams have come to realize something pretty obvious: as codebases grow, conventions alone stop being enough.
Not because all developers are careless (some are though), but because projects change, people join and leave and requirements evolve. Panta rhei, everything is in constant movement. The “obvious” thing from six months ago stops being obvious. At some point, you want parts of the system to be enforced by something more reliable than memory, discipline and social control.
That’s essentially what TypeScript did for JavaScript. JavaScript didn’t suddenly become a bad language, its loose behavior was there from the very beginning. We just started using it for large, long-lived systems. And thus we started to expect more from it. In a way TypeScript fixed JavaScript.
Then why is CSS in a different place?
We use it just as heavily as JavaScript. It’s just as essential to how our websites, webapps and components behave. CSS is too loose, much like how we didn't like JavaScript's weird type antics. A class name is either correct or incorrect, but CSS itself doesn’t know the difference. If you mistype a modifier, nothing fails. If you combine two variants that were never designed to be used together, the browser will happily apply both. If you change something in one place, pray to the almighty webgods it doesn't f up the entire styling of your application, haven't we all been there?
Then if all of this is true, why aren't we trying to fix just that?
Components have APIs — styling included
CSS started as a document styling language. The mental model is pages, selectors, and global rules. That model still exists, but it’s not how most teams think about UI these days. Now, most styling decisions happen at the component level.
When you build a component library, styling decisions become part of the public surface.
Take something simple like a button. In a design system, a button could have:
- A defined set of variants like size (small, medium, large) or intent (danger, success, neutral)
- Constraints about which variants combinations are valid
- Default variants and variant combinations
- Potential for structural parts/children like an icon, a loader or an extra label
Those are not random details at all, they are design decisions. And so design decisions become effectively part of the component’s API.
In CSS, we typically express this with conventions, like for example with BEM:
.ui-button { ... }
.ui-button--size-small { ... }
.ui-button--intent-danger { ... }
.ui-button__icon { ... }
...
This works... sort of. But it's all based on everyone playing nice. The system doesn't care what you write and how bad you f it up.
.uibutton-i-dont-care {
amount-of-cares-given: 0;
// Good luck catching this one in that one PR that has 5000 changed CSS files
}
<button class="ui-button uibutton--i-dont-care whatever">
Loom approaches this differently. Instead of relying on these agreed upon conventions, it lets you define variants and structure explicitly. Yes, that means it is extremely opinionated and it's not trying to hide it:
namespace ui;
type Size = 'small' | 'medium' | 'large';
class Button {
@size: Size = 'medium';
@intent: 'neutral' | 'danger' | 'warning' | 'success';
slot icon;
slot link;
{%
color: blue;
background-color: red;
%}
}
Here, size and intent are not just class name fragments. They’re typed properties with known value sets and defaults.
If a value isn’t part of the type, it simply doesn’t exist in the system and could throw an error or warn you on compile time. This is just a little example to peak your interest. But I'm experimenting with Loom to give answer to as much challenges that we're facing in this era of component-based development as possible.
Component libraries still struggle to expose "how to style this"
One thing I keep running into is that component libraries have gotten very good at exposing behavior, but they still struggle to expose styling. Or better, their "style system".
In React, Vue, Svelte, or Web Components, you can usually import a component and get:
- prop types
- events
- slots/children
- and more
But styling is different. The styling API is usually implied rather than explicit.
A component might support size="small" or intent="danger", but the actual styling system behind it is often implemented as:
- a set of class name strings you could overwrite
- a naming convention
- a bunch of options you could prop drill into the component
- some documentation in Storybook
- and the list goes on
And if you’re consuming the library in a different environment (across frameworks or even with a different team), there’s no standard way to "import the styling contract."
Simply put:
You can import the component. You can import its TypeScript types. But you can’t import the design system.
That gap is one of the main things Loom tries to address. Loom treats styling as a contract that can be exposed for anyone to consume in a safe way.
Because Loom is a language with types and structure, it becomes possible to export more than just CSS.
The obvious output is CSS, but the more interesting output is everything around it:
- Which variants exist
- Which variants are combinable
- Which values are allowed
- Which defaults are applied
- Which states are used
- What the component structure looks like
- which components extend others
For example, this component definition:
class Label extends Text {
@intent: Intent;
{%
background-color: #f0f0f0;
color: #000000;
%}
}
…contains enough information to generate:
- the CSS classes
- documentation for the design system
- .d.ts file
Exporting TypeScript types
One of the more practical ideas behind Loom is that it could export TypeScript types alongside the CSS. Not as a replacement for component prop types, but as a way to share the styling surface area across frameworks.
For example, Loom could generate something like:
enum ButtonClass {
Base = 'ui-Button',
Icon = 'ui-Button__icon',
Label = 'ui-Button__label',
}
type ButtonSize = 'small' | 'medium' | 'large';
type ButtonIntent = 'neutral' | 'danger' | 'warning' | 'success' | 'flashy';
interface ButtonStyles {
size?: ButtonSize;
intent?: ButtonIntent;
}
Do with these as you please: feel free to directly bind the output classes to your React components. A Vue library could use those types for component definitions. A design token tool could use those types for validation. The sky is still the limit!
Even if your UI components are implemented differently across frameworks, the styling contract could remain consistent because it’s coming from the same Loom source.
And whenever something changes in your initial contract, you will be notified of the impact on your components (potentially across stacks) instead of it just silently breaking.
Exporting design system documentation
Another practical benefit is documentation. Most design system documentation is written manually:
- a page for Button
- a list of supported sizes
- a list of supported intents
- examples of usage
- notes about structure
And manual docs are fine, until they drift.
Loom definitions already contain the information that documentation needs. So it becomes possible to generate docs automatically:
- Component name
- Variants, allowed values, defaults
- Slots and structure
- Inheritance
- Generated BEM classes
In other words, Loom can become a single source of truth for both:
- what the design system is
- and how it compiles to CSS
That doesn’t eliminate the need for human-written docs (especially around design intent), but it does remove a lot of the repetitive, error-prone parts.
Where Loom sits next to Sass
The most important thing Loom and Sass share, is that they both compile to CSS. Both extend the language and both are supersets of CSS.
I love Sass with all my heart. But Sass never wanted to be the tough guy, all Sass wanted to do was add syntactic sugar to make it easier, essentially being the fun guy. And it accomplished that. But Sass still suffers from the same problems as CSS and with widespread support for nested selectors and CSS variables, some of Sass' novelty has worn off. I see many teams now opting for vanilla CSS. I don't believe Sass is the jQuery of styling just yet, but one thing is clear: it will not give answers to all the challenges we're facing in this component-based era of webdevelopment. And Loom, while A LOT more opinionated, is trying just that.
Where Loom sits next to Tailwind
Tailwind approaches CSS from another direction. Instead of defining reusable semantic components, it encourages composing interfaces from small, atomic utility classes.
That works particularly well when speed and flexibility are priorities. It reduces naming overhead (even though it moves it to the component level) and keeps styles close to usage.
But where Tailwind fails to embrace the power of CSS, Loom shines. With Tailwind you're not really making design systems, you're just getting the job done quickly. For example, some component libraries copy their code into your codebase, so you can manually edit the Tailwind classes in the React component. How is that for seperation of concerns, wasn't that the whole point of CSS? To be able to switch out styles easily, to have a interactivity layer and a appearance layer? Why can't we have both components AND seperation of concerns?
I give credit to Tailwind for the approach, but it only works really well in one specific case. Loom and Tailwind, while not entirely opposing philosophies, clearly reflect different priorities.
Where Loom sits next to CSS-in-JS
CSS-in-JS addressed real problems in frontend development, especially around scoping and co-location. It allows styling to live next to component logic and often makes variant handling straightforward using JavaScript.
That's a strong model when styles are highly dynamic or closely tied to runtime behavior.
Loom is taking a different angle. It focuses on compile-time guarantees and predictable output. It doesn’t introduce a runtime layer. It compiles to static CSS. It’s also framework-agnostic.
If CSS-in-JS is about integrating styling into JavaScript, Loom is about strengthening the styling layer itself.
Both approaches recognize that large systems need structure — they just apply that structure in different places.
Why I’m building this
I’m building Loom because I keep seeing the same gap in component libraries and design systems.
We’ve gotten very good at shipping components that are reusable and well-typed at the JavaScript level. We can expose prop types, event types, and slot types. We can validate behavior. We can generate API docs.
But the styling system still lives in a parallel world.
Even in well-built component libraries, the styling contract is usually something like:
- “Here are the class names we generate”
- “Here are the modifiers we support”
- “Here are some examples”
- “Please don’t combine these two”
- “These are the only allowed sizes (we promise)”
And all of that is held together by conventions.
What I want is a way to define styling once — in a structured, typed form — and then export it into the different things a real team needs:
- BEM-style CSS for production
- TypeScript types for framework bindings
- Documentation for the design system
- Validation tooling
Loom is still experimental, and I’m not pretending it's the final answer. But I think it's worth exploring, you know? Treating component styling as a real contract, not just a bunch of class name strings.
At the very least, I'm curious what would happen if we treated design systems like actual code instead of just... conventions.
You can follow along with my experiments on Github:
https://github.com/reinvanoyen/loom-lang
Top comments (1)
Creative!