I really like the idea of a classless CSS stylesheet that can be applied to just about any website. There are already a few examples out there, as listed below. But none of them really ticked all the boxes for me. I wanted to have my own shot, so I made Ssstyles.
Classless stylesheets are ones that apply directly to HTML tags. I like their spirit of simplicity and separation of concerns. You can even mix and match styles looks across websites.
Most of the projects I stumbled upon work really well already, but there was still something missing, like themes for dark and high contrast mode, or configuration options. Some other features I included are:
- Color theme and high contrast mode
- Seamless Font scaling
- Configurable fonts
- Definition lists
- Dialog and Details/Summary
- Accent colors
When I put it together, I started experimenting and built some UI elements, that didn't fit into a classless stylesheet. But they were still handy to have. I added them as optional components, tree-shakeable via css imports.
Comparing Classless Stylesheets
Here’s a comparison between things that I wanted in a classless stylesheet. It favors Ssstyles, of course, because I made it fit my own needs. Take this table with few big grains of salt.
Name | File Size (gzip) | Configurable | Color Schemes | High Contrast Mode | Font Scaling | Extensions | Opinionated * |
---|---|---|---|---|---|---|---|
https://iamschulz.github.io/ssstyles/base-styles/ | 2.4 kB | Fonts, Colors, Border radius, Container width | ✅ | ✅ | ✅ (seamless) | ✅ | very |
https://watercss.kognise.dev/ | 3.5 kB | Colors, Scrollbars, Animation duration | ✅ | ❌ | ❌ | ❌ | a bit |
https://codepen.io/mblode/pen/JdYbJj | 2.9 kB | Fonts, Colors, Spacing, Border radius | ❌ | ✅ (default) | ❌ | ❌ | very little |
https://simplecss.org/ | 3.7 kB | Fonts, Colors, Border radius | ✅ | ❌ | ✅ (breakpoint) | ❌ | very |
https://v2.picocss.com/docs/classless | 9.3 kB | only in Sass | ✅ | ❌ | ❌ | ✅ | a little |
https://github.com/edwardtufte/tufte-css | 2.2 kB, but loads fonts | ❌ | ✅ | ✅ (default) | ❌ | ❌ | very |
https://github.com/marcop135/bullframe.css | 4.2 kB | only in Sass | ✅ | ✅ (default) | ❌ | ✅ | very little |
https://newcss.net/ | 1.9 kB | Fonts, Colors | ✅ | ✅ (default) | ❌ | ❌ | very little |
https://boltcss.com/ | 2.3 kB | Colors, Border radius | ✅ | ✅ (default) | ❌ | ❌ | very little |
https://yegor256.github.io/tacit/ | 1.8 kB | ❌ | ❌ | ✅ (default) | ❌ | ❌ | a little |
https://writ.cmcenroe.me/ | 1.0 kB | ❌ | ❌ | ✅ (default) | ❌ | ❌ | very |
https://raj457036.github.io/attriCSS/ | 1.4 kB, but calls a google font | ❌ | ❌ | ❌ | ❌ | ❌ | very |
(I got most of those projects from this CSS-Tricks article.)
(By opinionated I mean extravagant design choices that look like they take some extra steps to overwrite.)
I think I struck a good balance between a small file size and features.
Most of the projects I stumbled upon work really well already, but there was still something missing, like themes for dark and high contrast mode, or configuration options. Some other features I included are:
- Color theme and high contrast mode
- Seamless Font scaling
- Configurable fonts
- Definition lists
- Dialog and Details/Summary
- Accent colors
When I put it together, I started experimenting and built some UI elements, that didn't fit into a classless stylesheet. But they were still handy to have. I added them as optional components, tree-shakeable via css imports.
Components
Ssstyles brings a classless base package, but can be extended with optional components that hook into the base styles, so it all comes together seamlessly. That keeps the file size of the base package low and lets me experiment with UI components a bit more freely. The base package is meant to provide some baseline styles for new projects, as well as enhancing existing websites. That’s why I kept it classless, I want it to just work with plain old HTML.
However, just a classless Stylesheet wasn’t enough for me as a boilerplate for new projects. So I figured, I can just add some optional components and split the code. Now there’s the base stylesheet, the complete stylesheet and an npm package that lets you import individual components.
npm install ssstyles
@layer base, layout, components;
@import "ssstyles" layer(base);
@import "ssstyles/css/transition.css" layer(base);
@import "ssstyles/css/basegrid.css" layer(layout);
@import "ssstyles/css/headline.css" layer(components);
Since my stylesheet is meant as a baseline for projects to build upon, CSS Layers are an excellent tool to encapsulate my styles from project-specific ones. Having everything inside a layer keeps specificity low, so everything can be overwritten. Having my base
, layout
and components
layers lets me put my code inside components and still sort by their level of styling granularity:
-
base
includes rules which affect the whole website. The classless stylesheet, including the config variables go here, so do some variables I need for combined transitions across components. -
layout
includes rules which affect their children. This would include the base layout, but also grid components, breakouts, and so on. -
components
include rules which affect only their respective UI element.
On top of that comes everything specific to the project itself. You could even put something like Tailwind of top.
Fun with variables
I think variables were the single largest improvement in CSS in a very long time. They’re the answer to so many problems that would have been impossible to solve without. I use them to solve dependency across layers for two different aspects: transitions and border radius.
Transitions are grouped together into one property:
transition: box-shadow 0.2s, translate 0.2s;
That means that each component can only set one transition. That’s a problem, because some components are meant to be combined, for example Card and Shadow. A clickable card has a transition on its translate
, because it raises a few pixels when hovered. A shadow can be enlarged when hovered. This will set the transition to box-shadow
, overriding the translate
.
What I’ve done is set a transition for lots of animatable properties on a global selector. The transition time is set to a variables that defaults to 0s
, which disables the transition altogether.
Now I can set the transition time variable for position
to 0.2s
in the Card component and do the same for box-shadow
in the Shadow component. This will activate the transition for only the properties with a corresponding variable.
* {
transition:
transform var(--t-box-shadow, 0s) ease-out,
translate var(--t-translate, 0s) ease-out,
/* ... *(
}
[data-card] {
--t-translate: 0.2s;
}
[data-shadow] {
--t-box-shadow: 0.2s;
}
And now <div data-card data-shadow>
can have two different transitions.
I had a related problem with border-radius
. Normally I want to set it at the component level. Except for that one case when it’s dictated by its parent element: the Group. I want grouped elements to stick together and eliminate rounded edges in between. But since the component
layer comes after the layout
layer, the former would always overwrite the latter. I could use the infamous !important
on the group, but that would mean that no-one ever could enable them again for whatever reason without using !important
again later in the code.
Variables to the rescue!
I set the border radius in my components to variables with the pre-defined --border-radius
as a fallback:
border-radius: var(--br-tl, var(--border-radius)) var(--br-tr, var(--border-radius))
var(--br-br, var(--border-radius)) var(--br-bl, var(--border-radius));
When --br-xx
is unset, the --border-radius
from the config file is used in the respective corner (-tl
refers to top-left). That means that anything could set the --br-
variable without affecting the border-radius
property directly - even the Group that normally loses to elements on the component
layer.
[data-group] > *:first-child {
--br-tr: 0;
--br-br: 0;
}
[data-group] > *:last-child {
--br-tl: 0;
--br-bl: 0;
}
Now elements within a group only have rounded corners on the outside edges of the group itself.
Future plans
I’d like to include a thing that calculates themes automatically based on only a given hue and saturation value. I built something like that before, but it needed some correctional values for changing hues. Now that OKLCH is available and we have Level 5 color functions, that should be more manageable. Maybe also include a few themes as well, while I’m at it.
I also really wanted to build on typed CSS properties. Having type safe config variables sounds awesome! Sadly, I’m a bit early. Firefox will release the @property
ruleset with version 124, which is set to release in early 2024. As a typed config will be the very foundation of the stylesheet, I would end up either with duplicate code for a fallback, or have it not working at all in Firefox. I don’t like either way, so I’ll just wait a bit for some wider adoption of typed properties.
I'd also like to have a solution for variables in media queries. Right now I'm using a media query in the Base Layout, to break the navigation out of the content area to the side, when enough space is available. CSS variables in media queries don't work, because you could circle-reference variables. But that argument didn't hold water in the discussion about container queries either. I'm holding my hopes for the future. Until then, I can't really provide a way to configure this media query.
Top comments (4)
I think I like this, but I'm not sold.
I like the idea of having a base stylesheet which doesn't use classes, because every time I see someone write something like
<div class="nav">
a little piece of my soul departs.On the other hand, what you're doing is using data attributes instead of classes, which isn't functionally any different, is it?
I split this up into two parts. The classless stylesheet,
base.css
, doesn't use data attributes and should work with about every HTML document.I added some more, optional UI components that can be used with data-attributes. Those are available in the complete
all.css
or with npm. You're right, those data attributes are not functionally different from class names.There might be no functional difference, however data attributes do allow for better naming conventions. Also, simply by having the attribute exist you can already toggle certain styles on, do sort of a "base reset". This brings different mindset to what an utility is, and I'd argue can result in much better readability than you can ever get with classes. You can create your own concepts and expectations to aid the users of your utilities.
Personally to me things like this are "idiotic":
class="grid grid:inline grid:row"
While this makes much more sense:
data-grid="inline row"
And yes, I know the first example is kind of ported over from the latter example, but it shows how annoying it gets when you need to signal relation of classes. It also doesn't help that often the class names are attempted to be kept as short as possible precisely because there will be so many of them.
As for making such utilities you should have more purpose than just replicating CSS functionality directly, otherwise it would be pretty much the same to write
style="display: inline-grid;"
+ whatever the "row" would be equivalent to in this case.Even things like
data-grid="inline row"
seem overkill to me. Your theme layer is what knows how to display the content; you shouldn't need to tell it explicitly every time.