BREAKING WITH HABITS ć Manuel Matuzovič, Web Day Out, March 12th, 2026 Hello! Thank you so much for having me. It’s great to be here in Brighton. Today I’m here to talk about CSS. Three years ago, I was in Amsterdam to talk about CSS, as well,…

matuzo.social matuzo.at/cssday23 I gave a talk at CSSDay called „That’s not how I wrote CSS 3 years ago”. I explain about how I began styling websites with presentational elements and attributes in HTML, how I learned CSS in the early 2000s, and I mention some of my biggest milestones over the past 20 or so years. I also talk about the present, and I give an outlook for the future. I claim that many of the new features in CSS will significantly change the way we write and think about CSS. The thing is, that was three years ago, and nothing much has changed, at least for me, because I haven’t had a project yet that I could use to put my predictions into practice. That’s why I decided to create my own project and do everything I was talking about. I’m working on a no-class CSS framework called Oli and a web component library called Hanni, named after two of my kids. In this talk, I present my learnings and some of the things that already had a big impact on me.

matuzo.social Reset style sheets Structure and organisation Scalability Customisation Componentisation I want to focus on 5 main areas.

matuzo.social Reset Style Sheets I haven’t been using reset style sheets at all in the last five plus years or so because I just didn’t see the point.

matuzo.social ol, ul { list style: none; } reset.css by Eric Meyer

There used to be a time when reset style sheets were really useful because the browser landscape was more diverse than today, and browsers rendered pages very differently by default. Eric Meyer famously created the first popular reset stylesheet, reset.css, which removed margins, padding, and borders and reset other properties.

matuzo.social html { cursor: default; overflow y: scroll; webkit tap highlight color: transparent; webkit text size adjust: none; } normalize.css by Nicolas Gallagher Nicolas Gallagher took a gentler approach and normalised more than reset. That made sense and was super useful.

The thing is, that was a long time ago, and both style sheets solved many problems that don’t exist anymore today, but reset style sheets are still a thing. There are modern style sheets, such as “The New CSS Reset” by Elad Shechter.

matuzo.social #yo lo *:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) { all: unset; display: revert; } The new CSS reset by Elad Shechter His approach is to reset almost everything. He selects most of the elements and sets all: unset, and you can also see a lot of none and revert keywords in his style sheet. That’s pretty radical, but there are also other modern style sheets that take a different approach.

matuzo.social body { min height: 100vh; line height: 1.5; } A (more) Modern CSS Reset by Andy Bell For example, Andy Bell’s “A (more) Modern CSS Reset”. Looking at it and other similar style sheets, I realised that today it’s more about improving the defaults rather than resetting or normalising properties. I found that very interesting.

So, this is the first area where I’m challenging my usual approach. I’m trying to find out whether it might make sense to use a reset stylesheet in 2026 and whether modern CSS can solve any problems I may have.

*, * after, * before { box sizing: border box; }

: : : : Of course, in my reset stylesheet, I’m doing some of the basic, well-established stuff, like using the traditional box model, but now we are mostly interested in the new CSS stuff.

label:has( + :where( input:not([type=”radio”], [type=”checkbox”]), select, textarea) ) { display: block; } Here’s an example. If you create a label and an input field, it looks like this by default in the browser. The input field is placed next to the label. I would say that in 98% of cases, that’s not what you want, because we know that for UX and accessibility, it’s best to place your form elements below the label. That’s what this rule does. Every label that is followed by an input, select, or textarea sets display block. Of course, that doesn’t apply to radio buttons and check boxes.

matuzo.social Here’s another example: By default, if you open a dialog, it just appears and disappears without a transition. The problem is that the dialog is hidden, and we can’t animate hidden elements, or at least we weren’t able to do that for the longest time.

dialog, dialog backdrop { opacity: 0; transition: opacity 150ms ease out, display 150ms allow discrete, overlay 150ms allow discrete; } dialog[open], dialog[open] backdrop { opacity: 1; } @starting style { dialog[open], dialog[open] backdrop { opacity: 0; } }

: : : :

: : Today, it’s possible by not just transitioning opacity but also the display property using the allow-discrete keyword, which enables the animation or transition of discrete properties. The same goes for the overlay property, which allows us to fade out the dialog’s backdrop.

dialog, dialog backdrop { opacity: 0; transition: opacity 150ms ease out, display 150ms allow discrete, overlay 150ms allow discrete; } dialog[open], dialog[open] backdrop { opacity: 1; } @starting style { dialog[open], dialog[open] backdrop { opacity: 0; } }

: : : :

: : To allow our dialog to fade in as well, we have to define a starting style and set the opacity to 0.

Now you see a subtle, but nice transition in and out.

Another great example is popovers. By default, popovers are positioned at the centre of the screen, but we usually want them very close to the button that controls them. What’s great about popovers is that they come with an implicit CSS anchor. That means that you can position them without having to define an anchor explicitly,

button { anchor name: } the button; div { position anchor: the button; position area: end span end; position: absolute; }

Normally, you have to explicitly define an anchor and a reference in the element you want to position. That’s nothing we want in a base style sheet.

@supports(position area: end) { [popover] { margin: 0; position area: end span end; position try fallbacks: flip block, flip inline, flip block flip inline; } }

For popovers, you don’t have to do that because they come with implicit anchors. It just works. I’m removing the margin, and I’m using the position-area property to position the element, and I’m also providing fallbacks.

Now you can see how the popover is attached to the bottom of the button. And if there’s not enough space to show it below the button, it flips to the upper side of the button. That’s a much better default. You may want to revisit your existing style sheets, create one, and see what modern CSS can do for you.

matuzo.social fokus.dev/tools/uaplus My reset style sheet is available on GitHub, and I also created a website for it. It’s online at fokus.dev. Fokus with a k, because all the goods domain names are gone. The website is just a placeholder, but you will find a breakdown of all the rules I’m using, and I also have a demo that’s really cool. It includes most of the elements in HTML, and you can compare how it looks with my reset stylesheet and the browser’s default rendering, and you can also compare it against other style sheets.

matuzo.social Structure and Organisation The next big topic for me was structure and organisation.

matuzo.social @layer @property div#bu tton { @font-face backgr ound: re * {}} d !impo rtant p {} button {} .card [aria-current=page] #header .is-hidden !important ITCSS by Harry Roberts For most of my career I followed an approach very similar to Harry Robert’s ITCSS, which is represented with this inverted pyramid. The idea is that at the beginning of your CSS, you have generic code that applies to the entire website. In a pre-processor like Sass, that would be mixins, functions, etc. Then you get more specific by writing base rules using tag selectors. Then you get more specific by using selectors for your components, and finally, you have utility classes, which are very specific. The idea is that you avoid specificity problems by starting with very broad, low-specificity selectors and becoming more specific as your file progresses. The concept is great, but it’s just a concept. There isn’t anything that enforces these rules. If you put an id selector at the beginning of the file, it will mess with the specificity.

@layer core, third party, components, utility; @layer core.reset, core.tokens, core.base; @layer third party.imports, third party.overrides; @layer components.base, components.variations; / *

Luckily, now we have cascade layers that allow us to enforce these rules. Instead of having to manage the specificity for an entire document, you can now split the document into layers and manage the specificity inside these layers. Layers defined later in the document overwrite earlier ones. For my project, I didn’t just want to use cascade layers. I wanted to come up with a structure I can use for any project, of any size. Here is what it looks like. We have four main layers: core, third-party, components, and utility. base consists of three sublayers. One for reset style sheets, one for tokens, and one for base rules. The third-party layer consists of two sublayers. One for imports, so that’s where your bootstrap.css or prism.css belongs, and one for overrides. If there’s anything that you want to change in these third-party styles, you don’t do it just anywhere; you do it here, so there is one dedicated place for it. The component layer consists of two layers, one for the base and one for variations. * / Establish the order of layers upfront

@layer core, third party, components, utility; @layer core.reset, core.tokens, core.base; @layer third party.imports, third party.overrides; @layer components.base, components.variations; Reset, normalize, etc. @import url(‘uaplus.css’) layer(core.reset); / *

/ *

Here is how I add my reset style sheet by using the @import at-rule and importing directly into the core reset layer. * / / Establish the order of layers upfront

@layer utility { .visually hidden { clip: rect(0 0 0 0); clip path: inset(50%); height: 1px; overflow: hidden; position: absolute; white space: nowrap; width: 1px; } } / *

Here’s how I add a utility class to the utility layer. * / utilities.css

@layer components { @layer base { .card { size: 20rem; max inline size: var( } } size); @layer variations { .card large { size: 30rem; } } }

/ *

Here’s an example for component style. * / components/card.css

@layer core, third party, components, utility; / *

What’s great about that is that it doesn’t just make working with the Cascade easier, it also allows me to split up my CSS into multiple files. I don’t have to worry about specificity because in the very first line, we have already established the order of the cascade layers. If we use them later on anywhere in the documents, we’re not redefining layers, we’re just reusing them. * / Establishes the order of layers upfront

matuzo.social CORE THIRD-PARTY COMPONENTS UTILITY Here’s how my inverted pyramid looks: Base styles overwritten by third-party styles overwritten by component styles, and finally utility styles with the highest specificity.

matuzo.social CORE RESET TOKENS BASE THIRD-PARTY IMPORTS OVERRIDES COMPONENTS BASE VARIATIONS UTILITY Here’s the pyramid with sublayers.

matuzo.social fokus.dev/tools/css-boilerplate This CSS boilerplate is also online on fokus.dev. I have a dedicated page where I explain my decisions, and I also answer some common questions.

matuzo.social Scalability The next thing I wanted to find out is if there is a way to improve the resilience and scalability of my CSS.

matuzo.social handbuch.wien.gv.at One of my clients is the City of Vienna. I created the concept for their new pattern library and wrote much of it. The way we designed it is to use native HTML and CSS, and for JavaScript, Web Components. We are big fans of progressive enhancement. The website still looks and works fine without JavaScript. It even looks fine if you don’t add any CSS classes. That’s because we designed it as a no-class framework. That turned out to be really useful when we switched from our old CMS to a new one and automatically imported thousands of pages. Because that worked so well, I decided to pay much more attention to base styles than I used to.

h1 { font size: 51px; } h2 { font size: 40px; } h3 { font size: 32px; } …

Here’s an example: When I work with headings, I usually take the font sizes from the design and put them in my CSS. That’s fine, but just from the CSS, these sizes look like random numbers. But there could actually be a system behind these numbers.

matuzo.social precise-type.com If you look at this website that allows you to create type scales, you will see that, in fact, there is a system. You can pick a base font size, a scale, and the tool will give you different font sizes based on that combination. The tool starts with 16 pixels and multiplies it by the scale, 1.26. The result is 20; then it takes 20 and multiplies it by the scale again, yielding 25. Then it multiplies 25 by the scale, yielding 32, and so on. It’s easy to re-create that in CSS.

:root { scale: 1.26; base: 1rem; h6 h5 h4 h3 h2 h1 var( base); calc(var( h6) calc(var( h5) calc(var( h4) calc(var( h3) calc(var( h2) 16px * var( * var( * var( * var( * var( scale)); scale)); scale)); scale)); scale)); 20px 25px 32px 40px 51px } * * * * * / / / / / /

/

: : : : : :

I have a custom property for the scale and one for the base font size. My H6 equals the base size. The H5 is the result of the size of the H6 times the scale. The H4 takes the size of the H5 multiplied by the scale, etc. That’s fine, but I don’t like that for every font size, you have to reference the previous font size, and I also don’t like the repetition.

:root { scale: 1.26; base: 1rem; h6 h5 h4 h3 h2 h1 var( base); calc(var( base) calc(var( base) calc(var( base) calc(var( base) calc(var( base)


var( var( var( var( var( scale)); scale) * scale) * scale) * scale) * var( var( var( var( scale)); scale) * var( scale) * var( scale) * var( scale)); scale) * var( scale) * var( scale)); scale) * var( } Another way to write that is to multiply the base font size by the scale once for the h5. For the h4, multiply it twice, for the h3, three times, and so on.

: : : : : :

When I saw that, I was sure some dead dude had probably already come up with a formula for it. scale));

matuzo.social matuzo.at/pow I know that CSS now has math functions, so I went to MDN, and I tried to find the formula. I clicked on some random functions, and then I found this: “the Math.pow() static method returns the value of a base raised to a power.” That looked exactly like what I needed, so I tried it.

:root { scale: 1.26; base: 1rem; h6 h5 h4 h3 h2 h1 calc(var( calc(var( calc(var( calc(var( calc(var( calc(var( base) base) base) base) base) base)


pow(var( pow(var( pow(var( pow(var( pow(var( pow(var( scale), scale), scale), scale), scale), scale), 0)); 1)); 2)); 3)); 4)); 5)); } h1 h2 h3 h4 h5 h6 { { { { { { font font font font font font size: size: size: size: size: size: var( var( var( var( var( var( h1); h2); h3); h4); h5); h6); } } } } } } Instead of multiplying the scale multiple times, I now use the pow() function. The first value is the base number, and the second value is the exponent. The larger the exponent, the larger the font size. The number indicates how many times you want to multiply the base font size by the scale.

: : : : : :

Still a lot of repetition here, but much cooler repetition

:root { scale: 1.26; base: 1rem; } h1, h2, h3, h4, h5, h6 { font size: calc(var( base) * pow(var( } h1 h2 h3 h4 h5 { { { { { font font font font font factor: factor: factor: factor: factor: 5; 4; 3; 2; 1; scale), var( font factor, 0))); } } } } } To avoid repetition, I’m selecting all the headings and setting the font size to the base font size multiplied by the pow() function. The exponent defaults to 0, but you can override it using the —font-factor custom property. I know that looks really strange because we are used to using the font-size property, but this allows me to stay within my type scale. It enforces the sizing system in my CSS, much like what we did earlier with cascade layers.

I was really happy with that, but then I realised it only applies to headings, and I may want to use the system for other elements, too.

:root { scale: 1.26; base: 1rem; } *, after, before { font size: calc(var( base) * pow(var( scale), var( font factor, 0))); } h1 h2 h3 h4 h5 .big .small { { { { { { { font font font font font font font factor: factor: factor: factor: factor: factor: factor: 5; } 5; } 3; } 2; } 1; } 10; } -1; } Don’t try this at home, or actually try at home, but don’t try to work. Instead of selecting headings only, I’m selecting all the elements on the page, and I’m using my formula. It works fine for me, but it’s dangerous because it resets all font sizes and messes with inheritance. If I want a really large font size on a random element, I can just use a large exponent, or if I want a small font size, I can use a negative value.

: :

:

:

It’s cool that I’m now forced to use my type scale, but what if I don’t want to?

:root { scale: 1.26; base: 1rem; } *, after, before { _type power: pow(var( scale), var( font factor, 0)); _type scale formula: calc(var( base) * var( _type power)); font size: var( font size, var( _type scale formula)); } .big { font factor: 10; } .small { font factor: -1; } .custom { font size: 2rem; }

:

:

:

:

I’m still using my formula, but now it’s the fallback for a custom property. If the —font-size custom property is defined, that’s the font size. If not, it defaults to the formula.

matuzo.social matuzo.at/type-scale Here is what it looks like as an action. I have range sliders for the scale with a value between 1 and 2, and one for the base font size with a value between 10 and 20 pixels. As I’m changing the value, you can see how the scale adapts and looks different. It affects my .big and .small classes, but it doesn’t affect the .custom class. I don’t know about you, but I think that’s super cool.

:root { color color color color } main: #526d34; neutral: #636462; contrast: #8f4b57; alt contrast: #8c5700;

Another interesting topic is colour. For the most of my part of my career, I didn’t care much about colour and also didn’t know anything about colour theory, but Marc invited me to speak at Beyond Tellerrand in Berlin…

matuzo.social matuzo.at/fckafd … and I presented a talk titled “Color in CSS Or How I Learned to Disrespect Tennis”. In preparation for this talk, I learned a lot about colour theory and also how to use it in CSS. Now I know so much more about colour, but I still fucking hate Tennis.

:root { color color color color } main: oklch(0.5 0.09 130.31); neutral: oklch(0.5 0 0); contrast: oklch(0.5 0.09 9.76); alt contrast: oklch(0.5 0.11 69.33);

With my new knowledge, I switched to oklch() because it gives me more flexibility and I have access to much more colors.

The LCH color space is cylindrical. We have three channels. There is Lightness, with a value between 0 and 1 (0% and 100%). Chroma is a value between 0 and 0.4 The Hue angle is similar to the one in HSL, but they’re not identical. If you know that, the function is actually pretty easy to read.

With oklch, we now have access to 50% more colors, and all modern screens support that.

:root { color color color color } main: oklch(0.5 0.09 130.31); neutral: oklch(0.5 0 0); contrast: oklch(0.5 0.09 9.76); alt contrast: oklch(0.5 0.11 69.33);

You can see I have five custom properties with five different colours, but when we work with colour, we usually need many more colours, with lighter and darker variations of our base colours.

:root { color color color color color color color color color color } main: oklch(0.5 0.09 main-100 oklch(from main-200 oklch(from main-300 oklch(from main-400 oklch(from main-500 oklch(from main-600 oklch(from main-700 oklch(from main-800 oklch(from main-900 oklch(from 130.31); var( color var( color var( color var( color var( color var( color var( color var( color var( color main) main) main) main) main) main) main) main) main) 0.9 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1 c c c c c c c c c h); h); h); h); h); h); h); h); h);

: : : : : : : : :

With relative colour syntax in CSS, that’s easy to do. We use a colour function, extract the channels we need from our base colour, in this case the chroma and the hue, and then we change the lightness.

matuzo.social And this gives us this really nice colour scale for each base colour. They look nice, but they’re far from perfect. If you want to know why, watch my talk at Beyond Tellerrand or ask me later.

matuzo.social There are many more rules in my style sheet, but I would say that was the most interesting stuff. Here’s how my final demo HTML file looks like. [Scrolling through the page. URL: https://olicss.dev/demos/all] TODO: NEUES VIEO

matuzo.social That’s great, but this style sheet only includes styles for elements. This page doesn’t look like a proper website. There is a header and a main navigation, but they don’t look like it.

matuzo.social Customisation I could add rules for the header and the nav element, but that’s risky because there could also be a header in a section or a nav element within the main content. On the other hand, if I know how the markup is structured, I may want the header to look like a proper header. What I need is a level of customisation that lets me either leave it as is or get an optimised version. What I need is some kind of magic that lets me choose.

html { magic: true; }

That’s why I select the root element and I set —magic to true.

matuzo.social And now the header looks like a proper header and the list in the nav element looks like a navigation.

@container style( magic: true) { header, main, footer { _inline size: ((var( oli s page width) - var( oli s page max width)) / 2); _wrapper layout: max(0px, var( _inline size)); } main { _main margin: 0 var( } _wrapper layout); header { _header align items: center; _header justify content: space between; _header margin: 0 var( _wrapper layout) var( } oli s header margin block end); }

That’s possible thanks to container style queries. In this query I check if —magic is set to true. If that’s the case, I change the layout and styling of the header, navigation and other elements.

:root { oli s magic: true; oli s layout: horizontal; oli s font stack: transitional; }

Right now, I have three settings: magic, one for layout that allows me to switch to a horizontal layout and one that allows me to switch the font stack.

:root { _oli _oli _oli _oli _oli _oli } font font font font font font stack humanist: Seravek, “Gill Sans Nova”, Ubuntu, Calibri,”DejaVu Sans”, source sans pro, sans serif; stack geometric: Avenir, Montserrat, Corbel, “URW Gothic”, source sans pro, sans serif; family old: “Iowan Old Style”, “Palatino Linotype”, “URW Palladio L”, P052, serif; family slab serif: Rockwell, “Rockwell Nova”, “Roboto Slab”, “DejaVu Serif”, “Sitka Small”, serif; family transitional: Charter, ‘Bitstream Charter’, ‘Sitka Text’, Cambria, serif; family monospace slab serif: ‘Nimbus Mono PS’, ‘Courier New’, monospace; body { @container style( oli s font stack: geometric) { oli s font family: var( _oli font stack geometric); } @container style( oli s font stack: old) { oli s font family: var( _oli font family old); } @container style( oli s font stack: humanist) { oli s font family: var( _oli font stack humanist); } @container style( oli s font stack: slab serif) { oli s font family: var( _oli font family slab serif); } @container style( oli s font stack: transitional) { oli s font family: var( _oli font family transitional); } @container style( oli s font stack: monospace slab serif) { oli s font family: var( _oli font family monospace slab serif); } }

I’m using some of the font stacks from modernfontstacks.com. That’s basically a switch statement in CSS.

matuzo.social TODO: Ersetzen durch video

:root { oli oli oli oli oli oli oli oli oli oli oli oli oli } s magic: true; s layout: horizontal; s font stack: transitional; s scroll behavior: smooth; s page max width: 80rem; s page width: 100%; s page start column width: 16rem; s page text scale: 1.4; s page line height: 1.7; s page padding: 2rem; s page background color: var( oli g colors color-5); s page color: var( oli g colors black); s…

On top of my three custom properties with made-up values I also have a bunch of properties that I use directly without querying them. That gives me a high level of customisation.

matuzo.social Componentisation I didn’t come up with that on my own. Of course, I was inspired by the amazing Lea Verou. In a presentation, she discussed how custom properties can replace presentational attributes and classes in components. Here’s an example.

h3> <img src=”image.jpg” alt=”” class=”card media”> div> div> Let’s say we have a good old card component. Heading, some content, and an image.

/ <

/ < / < / < That’s how it looks like: An image followed by a heading and text. / <

<div class=”card”> <div class=”card inner”> <div class=”card content”> <h3 class=”card heading”>Demo <p>Content p> div>

If we want a larger variation of the same component, we set —card-size: l;

/ < And then we get a slightly larger card. / <

<div class=”card”> <div class=”card inner”> … div> div>

If we want a larger variation of the same component, we set —card-size: l;

/ < And then we get a slightly larger card. / <

<div class=”card” style=” card size: l”> <div class=”card inner”> … div> div>

card axis: x”> We can also change the layout by defining another property, —card-axis: x;

/ < That changes the layout and puts the image on the left and the content on the right. / <

<div class=”card” style=” card size: l; <div class=”card inner”> … div> div>

.card custom { card size: l; card axis: x; card scheme: dark } You don’t have to use inline styles as I did. You can also use a class and set the properties you need. The big difference is that we’re not redeclaring random properties; we’re configuring our predefined component settings.

There are also other advantages.

style=” style=” style=” style=” card card card card size: size: size: size: s”>… s”>… s”>… s”>… div> div> div> div> / / / / < < < <

Of course, working with classes is still fine, that’s not the point, but one of the advantages of using custom properties is inheritance. If you want all items in a parent item to adapt to a certain size, you can define the size on each child,… / <

<div class=”news”> <div class=”card” <div class=”card” <div class=”card” <div class=”card” div>

…, but you can also define it on the parent, and the children will react to it, because custom properties are inheritable.

/ / / / < < < < That enables you to influence the styling of multiple elements at once without having to predefine selectors. / <

<div class=”news” style=” card size: s”> <div class=”card”>… div> <div class=”card”>… div> <div class=”card”>… div> <div class=”card”>… div> div>

.card { _card max inline size: 13rem; max inline size: var( _card max inline size); } @media (min width: 48rem) { .card { _card max inline size: 23rem; } } .card large { _card max inline size: 23rem; }

Another advantage is that you can write DRYer code. Let’s say you define a max-width for the card. On a larger screen, you increase the max-width. You do the same in a custom class. You have to define the value once for the media query and than repeat it for the custom class.

.card inner { _card max inline size: 13rem; max inline size: var( _card max inline size); } @container style( card size: l) { .card inner { _card max inline size: 23rem; } } @media (min width: 48rem) { .card { card size: l; } } .card large { card size: l; } With container style queries, you define the value once, and then you just repeat the setting. That’s very similar to using mixins.

You set up a system once, and then you just configure it.

matuzo.social olicss.com Everything I showed you is online at olicss.com. It is not reading for production yet because I’m still working on it, and Firefox doesn’t support container style queries…

matuzo.social Firefox 148 …but they are already in Nightly, which means you’ll be able to use them soon. Firefox Nightly 150

slides: matuzo.at/ webdayout26

matuzo.social Thank you! ❤ accessibility-cookbook.com matuzo.social htmhell.dev matuzo.at manuel@matuzo.at Thank you!