Ssstyles - a classless stylsheet
Tags: css, webdev, ui, frontend
Published: 2024-01-08 09:26:13

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](https://iamschulz.github.io/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.

![A desktop screenshot of https://iamschulz.github.io/ssstyles/, showcasing they stylesheet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7clejsrc8l5gufd9ql89.png)


# 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](https://css-tricks.com/no-class-css-frameworks/).)

(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](https://cdn.jsdelivr.net/npm/ssstyles/dist/base.css), the [complete stylesheet](https://cdn.jsdelivr.net/npm/ssstyles/dist/all.css) and an [npm package](https://www.npmjs.com/package/ssstyles) that lets you import [individual components](https://iamschulz.github.io/ssstyles/#components).

```bash
npm install ssstyles
```

```css
@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](https://iamschulz.github.io/ssstyles/#include-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:

```css
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.

```css
* {
	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:

```css
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.

```css
[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](https://iamschulz.com/lights-out!-overengineering-a-dark-mode/) before, but it needed some correctional values for changing hues. Now that [OKLCH is available](https://caniuse.com/?search=oklch) and we have [Level 5 color functions](https://drafts.csswg.org/css-color-5/#color-mix), 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](https://caniuse.com/?search=%40property) 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.