Ssstyles - a classless stylsheet

Daniel Schulz - Jan 8 - - Dev Community

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.

A desktop screenshot of https://iamschulz.github.io/ssstyles/, showcasing they stylesheet

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
Enter fullscreen mode Exit fullscreen mode
@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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . .