https://dev.to/wintercounter/boss-css-i-created-another-css-in-js-lib-and-here-is-why-23kc
Skip to content
DEV Community
[ ] Powered by Algolia
Log in Create account
DEV Community
# Add reaction
[spa] Like [mul] Unicorn [exp] Exploding Head [rai] Raised Hands
[fir] Fire
Jump to Comments Save Boost
Copy link
Copied to Clipboard
Share to X Share to LinkedIn Share to Facebook Share to Mastodon
Share Post via... Report Abuse
Cover image for Boss-CSS: I created another "CSS-in-JS" lib, and here
is why!
wintercounter
wintercounter
Posted on Mar 2 * Originally published at bosscss.com
# # # # #
Boss-CSS: I created another "CSS-in-JS" lib, and here is why!
#react #tailwindcss #css #webdev
Boss-CSS is a polymorphic "CSS-in-JS" library supporting multiple
different ways of applying CSS to your codebase, with or without
runtime.
Before I get into the details there are a few things I want to
clarify. I'm not a content creator or writer, and I don't want to be.
For this reason, this will be one single, detailed, long ass post.
I'll try to explain and justify the choices I made inspired by my
past experiences. It'll also serve as some sort of initial
documentation.
In case you want to read about the library itself and skip its
history, jump here: Boss CSS
History
I'm coding mostly for web for 22 years now, and for 17
professionally. I've used several different ways to do CSS, using all
kinds of frameworks, methodologies, different styling solutions,
compilers and architectures. We always change for the better, trying
to overcome the problems our previous solution suffered from. But
this doesn't mean the new solution performs better in all areas,
always leaving a gap behind.
My first stop was using raw CSS. To solve organization issues, I
started to split files into multiple chunks. To simplify selector
usage, I've started to use LESS and SASS with support for nesting. To
solve issues with naming and avoid selector collision, I was using
methodologies like Atomic CSS and BEM. To simplify usage in the
codebase and enable tree-shaking, I started to use CSS Modules. To
improve DX, I started to use CSS-in-JS based solutions. To solve
runtime performance issues, I started to use build-time static
extraction. I started to use Tailwind, because it's lightweight and
framework agnostic. All the solutions are coming with their own
strengths and flaws, which would deserve their own articles.
Around 8 years ago, I chose Grommet as the UI layer for a project. I
immediately fell in love with their Box component, but I still ended
up writing custom style objects for specific needs. A few years
later, on another project, I ended up creating my own Box component.
At the end it got quite complex and still, it didn't give me full CSS
support, which again required me to write custom style objects and
custom class names in CSS files.
This was the time when CCSS and YouEye were born. It fundamentally
changed how I wrote and used CSS. I'm still using them in production
today, but (as it should) they started to reveal their own weaknesses
over time.
My relationship with Tailwind
The last couple of years it became unavoidable to work with Tailwind
on a project. I'm using it both in personal and professional
projects. It's a classic love-hate relationship. I always loved the
idea of utility classes and Atomic CSS. In fact, I've created my
first utility class based CSS framework on top of SASS 11 years ago.
It was generating utility classes based on configuration, then it was
purging the final CSS based on usage. The final results were actually
pretty close to Tailwind's AOT compiler.
However, I have some problems using Tailwind:
* I always end up in situations where I have to keep maintaining a
separate styling solution for custom needs. Tailwind is only a
subset of CSS and it won't cover every area.
* The naming of selectors: I wouldn't be able to count on my 2
hands how many times I've Googled the term tailwind line-height,
because of course, it's leading and I keep forgetting that.
What's font-size in CSS, it's text-[size] in Tailwind. These
minor differences are driving me crazy, and in some cases it's
seriously interrupting my flow.
* Dynamic cases: whether Tailwind or CSS Modules, I always disliked
merging class names using 3rd-party utilities, and by hand using
conditions.
* States and pseudo elements: I hate using hover 5 times just to
style my hover state.
* Breakpoints: I always hated mobile-first approaches, especially
nowadays when it doesn't really make sense anymore.
Please note that at the time of writing this, we only had
Tailwind v2 available. It came a long way since then, solving
many problems, especially around custom CSS.
I understand those who are in favor of Tailwind over native CSS. It
actually solves a lot of quirks and issues coming with native CSS. I
believe it's a much better choice for those who started web
development in the last couple of years or have a more backend-heavy
background and feel uncomfortable writing CSS directly.
However, I spent a lot of years learning, understanding and
overcoming these problems. In most cases, I simply want to write CSS,
and Tailwind just doesn't give me that.
Today
I actually never intended to write another CSS-in-JS library. But
over the years I had so many thoughts about what I'd do differently,
how I'd solve certain problems. Sometimes I had sleepless nights
thinking about styling in different ways. Over time, these thoughts
piled up and I've started to connect the dots in my mind. Last year,
during such a night, I've decided to get up and test a few things in
an online playground. It was the moment when I decided to start this
new project, because I felt like I could offer something new.
The biggest question for me: which solution made me the happiest, the
most productive, and had the best possible balance in terms of both
DX and UX? For me it was prop-based CSS-in-JS with static and dynamic
extraction.
Since working remotely I've joined several short-term projects either
as a developer or consultant. I've seen dozens of cases where
projects had their own Box component, each of them suffering from
different flaws. This was also a fueling factor, because I know it
could be better, I did use better in the past.
Boss was born
When I first started to work on this new library, my goal was to
create a solution that's tooling independent. YouEye and many
existing tools are mostly relying on custom Babel plugins by parsing
and modifying AST at build time. I moved away from Babel years ago
due to performance reasons, first to ESBuild then to SWC. I wanted a
way that works regardless of the stack a project is using. Tailwind
introduced such a way: parse on your own.
Initially, I simply wanted to have prop-based CSS-in-JS that's
extracted as Atomic CSS, while having a lightweight runtime that
doesn't emit or manipulate stylesheets. I also wanted to make sure
that it's fairly easy to extend its functionality, which led me to an
event-based solution for its overall architecture. Soon I realized
that this deep control enables me to add support for different
syntaxes and extraction strategies, so users can decide which
combination suits their project best. This is the reason for the name
Boss, it lets you be in control.
I spent a lot of my free time working on the library. Unfortunately,
I don't really have much time next to my family and work, which led
to very slow progress. At the beginning of 2024 I also had some
personal issues, which forced me to pause every personal project.
This slow progress definitely caused some fragmentation in my code.
However, I spent so much time working on this and I truly believe it
offers something new, I simply don't want it to go to waste.
For the above reasons I decided to bring the project into shape for
an early Alpha release. I did, it was 90% ready. However after 3
months of sleepless nights and grinding next to work and the fam, I
hit a rough burnout phase. Only some minor bugfixes, documentation,
website, playground and such were left. I spent so much time away
from the project that I ended giving it up.
Last year I slowly moved over to vibe-coding. Today, I mostly just
review and instruct the agent's work. I decided to "revive" the
project in the middle of boredom. This time with AI only, taking care
of all the missing parts. Well, from this point I quickly got into
the rabbit hole, asking the AI to add features I previously planned
to implement in a future version, features I didn't even want to
implement. It only took ~4 weeks of some free time to finish all
this. Not the best quality of results, but it works enough.
My hope is that it'll gain some traction in the community, which
would force me to find a way to work on it more seriously. I hope to
get some help to shape its rough edges, and help to build reliable
documentation. In case those things don't happen, I still wanted to
put it out there and hopefully inspire fellow developers with some
ideas my solution brings to the table.
Honestly, I'm not even sure if the lib has relevance nowadays in this
AI era.
Boss CSS
When building Boss, I tried to bring the best parts of different
styling solutions I used over the years. While that is subjective, I
believe I achieved that for the most part.
It's just CSS
I often see libraries and frameworks coming with custom props, custom
namings for some cases, like the p and m prop only for margin and
padding, but nothing else, or consolidating font-* and text-*
properties under a single prefix. These add extra learning curves,
and can disrupt the flow of those coming with a heavy native CSS
background.
Parity with CSS naming and syntax was one of the most important goals
for me. I wanted to be able to write CSS as it is as much as
possible, without having to translate it in my head to some custom
syntax. Class name based styling is basically almost inline styles by
look.
If you know CSS, you know 98% of Boss.
Multiple syntax support
I'm going to showcase all the different syntaxes supported out of the
box.
Why support multiple syntax? Because it lets you mix-n-match
according to your own preferences, and your own specific needs/use
cases.
Prop based styling
<$$ color="red">
Prop-based styling is the default for me for a long time now. I
simply love it more than anything else. It comes with great
flexibility, handles dynamic cases very well, is highly customizable
for different design system needs, and can be perfectly typed.
Read more in the Docs: JSX usage.
Class name based styling
Using class names is really convenient, but when it comes to dynamic
cases, this is where it can get messy. In Boss, using class names
really just looks like using inline styles. It has almost identical
syntax, with the additional benefit of targeting states, pseudo
elements, media queries, keyframes, tokens, and more.
Read more in the Docs: Classname usage.
Object based styling
It's basically prop-based styling with a different syntax, but if you
prefer objects instead of props, no problem. (I know in this example
it doesn't look any different from an ordinary style object, and
that's a good thing.)
Prepared components
$$.MyStyledContainer = $$.$({
width: 1200,
})
const Component = () => <$$.MyStyledContainer>...$$.MyStyledContainer>
A concept Styled Components popularized in the first place, and it's
really handy to create reusable elements.
Read more in the Docs: Prepared components.
Raw CSS
If needed, you can still append raw CSS blocks with $$.css template
string/object forms. It remains useful for custom selectors and
one-off rules. Because sometimes we still need it.
$$.css(`
#element .selector ul li {
/* my custom css */
}
`)
Because sometimes we need it.
Mixed
<$$.a
className="display:flex place-content:center"
color={linkColor}
hover={{ background: 'red', borderBottom: '1px solid #000' }}
>
Click me!
$$.a>
Bosswind
An optional plugin that provides Tailwind-like shorthands and tokens
out-of-the-box.
// As classNames
<$$ className="flex px:1 text:xl" />
// As props
<$$ flex px={1} text="xl" />
In this example I used class names for static styles, and props for
dynamic cases, as well as the hover prop instead of multiple hover:*
classes.
Read more in the Docs: Bosswind.
Runtime?!
As you might have seen in the previous examples, prop-based styling
is done through a custom component, which might make you think
runtime is required. And you're not wrong. However, you have the
option to compile away all the components to native elements in a
bundler-agnostic way, or just use class names, which doesn't need
runtime.
Why I call Boss a polymorphic CSS-in-JS library is because Boss
includes runtime based on your usage. In case you're only using class
names with classname-only, runtime files won't be generated at all.
Plugins can determine when and what code to include based on how
you're using styles. This makes it possible to use Boss class names
in basically any stack/environment, regardless of framework or
programming language.
Having a lightweight runtime can be just as good as not having any,
but the most important part is that it can give superpowers. There
are many never-seen features in my head utilizing runtime, and I'm
eager to share them over time. One I can share is the experimental
devtool: when you select a DOM element on the page, you can adjust
its styling or source code right in the browser, hit save, and it
writes the changes back to the source file, without leaving the
browser.
This polymorphism also applies to TypeScript types and AI files. Boss
generates TS types, AI docs, and AI skills based on your
configuration, updating them based on your usage. It supports types
for custom properties, prepared components and design tokens as well.
Boss runtime also works differently compared to classic CSS-in-JS
solutions. In inline-first and classname-first it does not generate
full stylesheets on every render. In runtime-only mode it can inject
only the needed rules on the client. You can also go hybrid, and have
both pre-generated CSS at build time and runtime support for highly
dynamic cases, for example when users modify styles or certain rules
come from a 3rd-party source. This is one of the biggest ideas behind
the architecture.
Read more in the Docs: Runtime strategy.
Extraction strategies
Boss supports different extraction strategies. Again, this is to
facilitate different needs.
Inline first
This is the default strategy. Boss prioritizes inline styles, putting
everything possible inline, saving a large chunk of external, initial
and unused CSS.
Performance is arguable. You can find studies saying inline styles
are faster and just as many saying it's slower. In my opinion it's
circumstantial. But even if it tends to be slower in some cases, you
save time on loading external stylesheets, which are normally
blocking requests delaying FCP.
Input:
<$$ color="red" hover={{ color: 'yellow' }} />
Output:
In the above case we only have external CSS for the hover state.
Class name first
This is the opposite of inline first. It uses class names wherever
possible and keeps inline only where unavoidable (for example
user-flagged dynamic cases using CSS variables). This approach is
close to Tailwind-like output.
Input:
<$$ color="red" hover={{ color: 'yellow' }} />
Output:
Fully runtime
There are projects which need full runtime support and need to
generate styles on the fly. Projects with complex WYSIWYG editors or
page styles coming from separate databases. Full runtime support is
for these cases.
Boss supports this with the runtime strategy (runtime.only: true) and
also supports hybrid mode (runtime.only: false) when you want server
CSS output plus runtime dynamics.
Zero runtime extraction
It is possible to transform source to native HTML elements, classes
and style objects at build time, leaving runtime out completely.
Today this can be achieved through:
* classname-only strategy for static class usage
* npx boss-css compile for source transformation and runtime
pruning in supported cases
npx boss-css compile
Simple compile example:
// input
const Example = () => <$$ color="red" padding={8} />
// output
const Example = () =>
Read more in the Docs: Compile, Classname-only strategy.
Framework-agnostic approach
What I dislike about many extraction tools is that they are mostly
tied to a custom compiler integration (Babel plugin, framework
plugin, bundler lock-in). Boss is intentionally different here.
Boss is built to run through PostCSS, CLI build/watch, or compile
mode, without requiring Babel/Webpack/Vite-specific plugins. That
makes it easier to carry the same styling API across React, React
Native, Next, Preact, Solid, Qwik, Stencil, and non-JSX/non-JS setups
using static class names.
Read more in the Docs: Tooling agnostic.
Goodies
It's time to write about other random features and goodies Boss
supports.
$$ is global
By default, you can access Boss components and utilities using the $$
global variable. Whoever is old enough to have worked with jQuery
can't argue that accessing $ everywhere was pure simplicity, and with
inline styles the feeling is no different. Besides, you don't want to
import Boss in every single file where you write markup, just as much
as you would not want to do import { div, a, span, section } from
'react'.
Powerful plugin system
It's so powerful that basically every feature of Boss is just a
plugin. The core is mainly the event system. Plugins are split into
server/browser code: server controls compile-time tasks, browser code
ends up in your production bundle when needed.
Example for a simple custom prop plugin:
// .bo$$/plugins/bleed/server.js
export const name = 'bleed-prop'
export const onBoot = api => {
api.dictionary.set('bleed', {
property: 'margin',
aliases: ['bleed'],
description: 'Custom bleed prop',
isCSSProp: true,
})
}
export const onProp = async (api, { name, prop, contexts }) => {
if (name !== 'bleed') return
api.css.selector({
className: api.contextToClassName(name, prop.value, contexts),
})
api.css.rule('margin', prop.value)
api.css.write()
}
// .bo$$/config.js
import * as bleedProp from './plugins/bleed/server'
export default {
plugins: [bleedProp /* ... */],
}
<$$ bleed={-14} />
Numeric values use Boss's built-in unit conversion (api.unit), so
there is no need for manual ${v}px.
Read more in the Docs: Plugin hooks.
CLI tools
Boss also ships CLI tools for the full workflow:
npx boss-css init
npx boss-css build
npx boss-css watch
npx boss-css compile
npx boss-css dev
So whether you're on PostCSS or a custom/non-PostCSS setup, you still
have a clear path to bootstrap, build, watch, and optimize.
Read more in the Docs: CLI.
AI ready
Boss has built-in AI readiness through the AI plugin. It can generate
LLMS.md plus an AI skills bundle from your live configuration and
usage metadata (tokens, breakpoints, prepared components, strategy
context, and more).
This means agents can work with project-specific styling context, not
just generic guesses.
Read more in the Docs: AI plugin.
Fontsource plugin
Boss can load fonts from the Fontsource catalog through the
fontsource plugin, and injects the needed CSS when fonts is
configured.
import * as fontsource from 'boss-css/fontsource/server'
export default {
plugins: [fontsource, /* ... */],
fonts: [{ name: 'Inter', token: "body" }],
}
<$$ fontFamily="body" />
Read more in the Docs: Fonts and Fontsource.
Grouped selectors
You can use multiple styles inside one state/pseudo while using class
names. I have to admit that generated selectors for these are not
always pretty. But it works, it's there, and it's nice to have.
Besides, if you use compiler, that will split up such selectors for
you.
At (@) prop and breakpoints
You can write responsive styles with at and with shorthand
breakpoints.
<$$ at={{ 'mobile+': { fontSize: 18 } }} />
<$$ tablet={{ fontSize: 18 }} />
ClassName equivalent:
Built-in defaults include micro, mobile, tablet, small, medium,
large, and device, with name, name+, and name- forms.
It also supports arbitrary values and ranges, such as mobile-800.
Boss also supports container queries through container,
containerType, containerName, and container_* shorthands.
<$$ containerType="inline-size" containerName="card">
<$$.div container_card={{ mobile: { fontSize: 18 } }}>Child reacts to the parent container$$.div>
$$>
ClassName equivalent:
Child reacts to the parent container
Read more in the Docs: Pseudo and @ / responsive contexts.
Design tokens
You can access tokens in different ways:
* <$$ color="primary">
* <$$ color={$$.token.color.primary}>
*
*
TypeScript hints include available token values for each property.
Boss also supports DTCG/Style Dictionary token JSON and resolves
references/inheritance, which makes existing design token pipelines
easier to plug in.
On top of this, Boss supports local token theming with the tokens
prop, so you can scope theme overrides to a subtree. This is also
best shown as a parent override + child inheritance case:
<$$ tokens={{ color: { brand: '#ea580c' }, spacing: { md: 20 } }}>
<$$ color="brand" padding="md">
Parent with local token override
$$>
<$$.span color="brand">Child inherits the same scoped token override$$.span>
$$>
Token strings also support alpha suffixes, for example:
<$$ color="gray.600/60" />
And for DTCG users, composite token types like border, shadow,
transition, gradient, and typography are normalized into CSS-ready
values.
Read more in the Docs: Tokens.
Powerful TypeScript types
Boss generates type-safe CSS props from real CSS definitions,
token-aware props, and prepared component types into .bo$$/
index.d.ts.
You also get:
* Token autocompletion and token value hints
* Prepared component typing/JSDoc metadata
* Native runtime typings (native.d.ts) when React Native runtime is
enabled
Passing styles using spread
Boss supports marker/helper APIs for spread-heavy patterns:
const props = $$.$({ color: 'red', hover: { color: 'purple' } })
<$$ {...props} />
For plain elements, use $$.style(...):
Read more in the Docs: Spreads and markers.
Target children
Boss supports arbitrary nested selectors both in JSX (child) and
class names ([ ... ] syntax).
<$$ child={{ '.title': { color: 'blue' }, '&>div': { color: 'red' } }} />
Read more in the Docs: Classname usage.
Server components ready
Boss works with modern server-first frameworks (including Next.js
patterns). For Next.js you wire runtime with instrumentation hooks
and import CSS manually when css.autoLoad is disabled (which init
does by default for Next).
Read more in the Docs: Next.js integration.
Style boundaries
Boss can split generated CSS by directory using *.boss.css
boundaries. This lets you load only what you need while shared rules
can hoist to common ancestors or global styles.
src/
app/
app.boss.css
page.tsx
admin/
admin.boss.css
page.tsx
.bo$$/styles.css
// src/app/layout.tsx
import './app.boss.css'
Boundary files stay empty in source and are overwritten by Boss on
build/watch/postcss runs.
Read more in the Docs: CSS boundaries.
React Native
I've used YouEye in Expo projects, creating cross-device components
and I loved it. You can achieve the same with Boss as well.
Boss can generate native runtime output (.bo$$/native.js, .bo$$/
native.d.ts) using the same $$ API while mapping props to React
Native styles/components.
// .bo$$/config.js
import * as native from 'boss-css/native/server'
export default {
plugins: [native /* ... */],
}
export default function App() {
return (
<$$ padding={12}>
<$$.Text>Native UI$$.Text>
$$>
)
}
Read more in the Docs: React Native usage.
Use it as a companion to Tailwind
Boss class names do not collide with Tailwind's by default, which
means you can use them together. You can use Boss for CSS cases
Tailwind doesn't support, or to handle dynamic cases more nicely.
If you want a more Tailwind-like flavor inside Boss itself, there is
Bosswind for aliases/booleans on top of the same pipeline.
Variants and class composition
Boss includes $$.cx, $$.cv, $$.scv, $$.sv (and direct imports) for
class/style composition patterns. This makes conditional/variant
flows cleaner than hand-rolled class merging.
The $$ component's className also has built-in support for cx.
<$$ className={{ 'display:none': isHidden }} />
Read more in the Docs: Class composition and variants.
Gotchas
Boss has its own gotchas as well. A few important ones:
* ClassName parsing supports static strings only. Dynamic template
interpolation is intentionally skipped.
* In runtime.only, className parsing is disabled (runtime only
handles props and nested contexts).
* classname-only has zero runtime, so no dynamic function values.
* Compile mode currently focuses on JSX and the main extraction
strategies.
* ClassName autocomplete in TS is naturally limited because strings
are strings. ESLint helps here.
ESLint plugin
This one already exists now, and I consider it essential. Boss can be
used in many different ways, thus it can be abused just as much.
Linting gives control to your projects to keep everything in line.
Read more in the Docs: ESLint plugin.
Closing notes
Why "CSS-in-JS" is in quotes
Boss sits in an in-between space. Depending on strategy, it can look
like classic CSS-in-JS, static extraction, utility-class generation,
or runtime-only behavior. That's why I keep putting it in quotes.
Generated files
Generated files can look messy. Necessary? In my opinion, yes
(kinda). They are the practical output of the polymorphic approach,
and they keep source authoring clean while letting the pipeline adapt
to usage.
What's next?
Nothing. I'm not intending to work on this project for now. I just
wanted to put it out there, and hopefully inspire some people with
the ideas behind it. If it gains traction, maybe I'll find a way to
work on it more seriously, but for now, it's done. I'm using it
myself in a project now, it helps reveal some rough edges and bugs
which I might or might not fix.
Links
* Website: https://bosscss.com
* Repository: https://github.com/wintercounter/boss
* Playground: https://bosscss.com/playground
The playground is WebContainer-powered and runs a full Vite + React
project in the browser, with editor, terminal, and preview. It also
has ready starter templates and shareable URL state, so you can
deep-link a template (https://bosscss.com/playground#template=
boss-bosswind) or share your exact workspace setup. If you're
curious, this is the fastest place to try ideas, push edge cases, and
see how Boss behaves before touching your local setup.
Top comments (0)
Subscribe
pic
[ ]
Personal Trusted User
Create template
Templates let you quickly answer FAQs or store snippets for re-use.
Submit Preview Dismiss
Code of Conduct * Report abuse
Are you sure you want to hide this comment? It will become hidden in
your post, but will still be visible via the comment's permalink.
[ ]
Hide child comments as well
Confirm
For further actions, you may consider blocking this person and/or
reporting abuse
[https] wintercounter
Follow
* Joined
May 16, 2018
Trending on DEV Community
Matt Lewandowski profile image
I Built a Microservice Name Generator Because Naming Things Is Hard
#webdev #programming #ai #architecture
Buono Make Studio profile image
What should we do in AI era?
#ai #career #discuss #vibecoding
Sarvar Nadaf profile image
I Let an AI Agent Become My DevOps Engineer
#aws #ai #discuss #devops
DEV Diamond Sponsors
Thank you to our Diamond Sponsors for supporting the DEV Community
Google AI - Official AI Model and Platform Partner
Google AI is the official AI Model and Platform Partner of DEV
Neon - Official Database Partner
Neon is the official database partner of DEV
Algolia - Official Search Partner
Algolia is the official search partner of DEV
DEV Community -- A space to discuss and keep up software development
and manage your software career
* Home
* About
* Contact
* MLH
* Code of Conduct
* Privacy Policy
* Terms of Use
Built on Forem -- the open source software that powers DEV and other
inclusive communities.
Made with love and Ruby on Rails. DEV Community (c) 2016 - 2026.
DEV Community
We're a place where coders share, stay up-to-date and grow their
careers.
Log in Create account
# # # # #