# data[].attributes.blurb.html
Some new features in CSS only show their real potential when we radically break with old habits. Doing that on an existing website is almost impossible. That’s why Manuel built a new project from scratch with modern CSS and questioned every line of code he wrote.

In this talk, he presents what he has learned and encourages you to review your best practices.

# data[].attributes.slidedeck.data[].slides[].desc.html
Hello! Thank you so much for having me. It’s great to be back here in Amsterdam.

# data[].attributes.slidedeck.data[].slides[].desc.html
The last time I was here was exactly three years ago with a talk titled “That’s not how I wrote CSS three years ago”. The whole idea of the talk was that in the years leading up to the event, the way I write CSS has changed significantly. And I explain how and why. And I also predict it will change even more with many of the new features in modern CSS, thanks to the Interop efforts. The thing is, it didn’t happen. At least not for me. And there are several reasons for that. The first reason is that I just didn’t have a project where I could put my ideas and predictions into practice. The second reason is that I’ve realized that I’ve been following certain principles, approaches, and an understanding of what’s a good and a bad practice for 10, 15, or even 20 years without second-guessing them. And for some things, that totally makes sense. Progressive enhancement was a great idea 20 years ago, and it’s a great idea today. But to get the most out of some new features in CSS, we may have to reevaluate how we approach problems in CSS. Here’s an example. I wrote a blog post about cascade layers a while ago, posted it on social media, and someone replied and said, “Hey Manuel, thank you. That was really useful. I tried it out, I put my CSS in layers, but I didn’t see any difference”. I’m not sure what they expected, but we chatted a little bit, and I learned that they were using BEM. So, they already had a tool in place that solves the problem cascade layers are supposed to solve. You know, making working with the cascade and specifically specificity easier. For this person to really see a difference, they would probably have to stop using BEM and change how they write their CSS selectors.

matuzo.at/cssday23

# data[].attributes.slidedeck.data[].slides[].desc.html
And this is why I came up with this talk and why I named it “Breaking with Habits”. Now, before I start, I have to make a quick last-minute change. This year I’ve been to several events and many of them featured talks about AI, and I also had a lot of conversations about AI. I heard some things I can’t get out of my head that I want to share with you. I talked to a friend who’s really into AI, and he told me that ever since he fully integrated AI into his workflows, he’s had more work than ever. Listening to him, I tried to figure out why that was, but then he continued and said, “But now I’m doing the work of five people”. It took me a while to process that, but now I’m asking myself, “Is that what we want?” Do we want to improve efficiency so that we can burn out at age 25 instead of 35? Is that our goal? Another thing I’ve heard someone say is that they have six agents running at the same time, working for them. One is their CEO, one is a project manager, another is an engineer, and another is an assistant who gives them a summary of their RSS feed every morning. And when I heard that, I was like, no, that’s not what RSS is about. It’s not about information consumption, it’s about reading and learning from your friends, the people you follow or respect. It’s about learning what’s on their mind, how they have solved certain problems, and also learning about their personal takes, the language that they use, and so on. Finally, and then I promise I will talk about CSS, I heard about someone who hasn’t touched their keyboard for coding in two years. So they’re only using their voice and prompting. And at times, I feel like maybe I’m out of the loop. Maybe I don’t know how web development works in 2026. Maybe I’m already developing the old way. But then again, if that’s the future, I’m not sure if I want it because I’m in the very privileged position, probably like most of you, that I really enjoy my job. I enjoy coding, building websites, and playing around in DevTools, designing in the browser. And I don’t see how prompting is as fun as doing the manual work. Also, the reason I was invited last time here was that I wrote 100 blog posts about CSS in 100 days. I did that because I was completely out of touch, and I needed to get up to speed with the latest developments in CSS. That’s already really hard, but if I don’t do any coding anymore, if I only let my AI thing do the coding, I’ll forget how to write CSS, and I won’t evolve as a developer. How will I be able to assess the quality of the code it produces? I know that’s a bit off topic, but I feel like I had to say this, and all of that is the reason why I decided to change the title of my talk. Don’t worry, PPK, not the talk, it’s still the same, just the title. And you can pick one of the following.

# data[].attributes.slidedeck.data[].slides[].desc.html
How about mental health is more important than efficiency?

# data[].attributes.slidedeck.data[].slides[].desc.html
Or “Read, learn, grow”, but maybe that sounds too much like a wall sticker…

# data[].attributes.slidedeck.data[].slides[].desc.html
Here’s one that I truly believe but might be a bit controversial.

# data[].attributes.slidedeck.data[].slides[].desc.html
Or what about “Your profound expertise matters”.

# data[].attributes.slidedeck.data[].slides[].desc.html
Or maybe just “Explore”, because exploring is what I did. I was trying to identify areas where I could break habits to get more out of CSS.

# data[].attributes.slidedeck.data[].slides[].desc.html
I want to focus on 5 main areas.

Reset style sheets

Structure and organisation

Scalability

Customisation

Componentisation

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

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

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
To allow our dialog to fade in as well, we have to define a starting style and set the opacity to 0.

@starting-style {
  dialog[open],
  dialog[open]::backdrop { opacity: 0; }
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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,

# data[].attributes.slidedeck.data[].slides[].desc.html
Now you see a subtle, but nice transition in and out.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

button {
  anchor-name: --the-button;
}

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

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

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

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
Oh, and one more thing. I’m wrapping all my rules inside a cascade layer, which is really cool because if you’re not using cascade layers in your project, all your unlayered styles will override the layered styles. And if you’re using cascade layers, you can just take the name and reference it or simply import the file.

To sum it up: You may want to revisit your existing style sheets, create one, and see what modern CSS can do for you.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
The next big topic for me was structure and organisation.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

/* Establishes the order of layers upfront */
@layer core, third-party, components, utility;

# data[].attributes.slidedeck.data[].slides[].desc.html
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 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);

# data[].attributes.slidedeck.data[].slides[].desc.html
Here’s how I add a utility class to the utility layer.

/* utilities.css */

@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;
  }
}

# data[].attributes.slidedeck.data[].slides[].desc.html
Here’s an example for component style.

/* components/card.css */

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

  @layer variations {
    .card-large {
      --size: 30rem;
    }
  }
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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 */
@layer core, third-party, components, utility;

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
Here’s the pyramid with sublayers.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
The next thing I wanted to find out is if there is a way to improve the resilience and scalability of my CSS.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

h1 {
  font-size: 51px;
}

h2 {
  font-size: 40px;
}

h3 {
  font-size: 32px;
}
…

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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: var(--base); /* 16px */
  --h5: calc(var(--h6) * var(--scale)); /* 20px */
  --h4: calc(var(--h5) * var(--scale)); /* 25px */
  --h3: calc(var(--h4) * var(--scale)); /* 32px */
  --h2: calc(var(--h3) * var(--scale)); /* 40px */
  --h1: calc(var(--h2) * var(--scale)); /* 51px */
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

:root {
  --scale: 1.26;
  --base: 1rem;

  --h6: var(--base);
  --h5: calc(var(--base) * var(--scale));
  --h4: calc(var(--base) * var(--scale) * var(--scale));
  --h3: calc(var(--base) * var(--scale) * var(--scale) * var(--scale));
  --h2: calc(var(--base) * var(--scale) * var(--scale) * var(--scale) * var(--scale));
  --h1: calc(var(--base) * var(--scale) * var(--scale) * var(--scale) * var(--scale) * var(--scale));
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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;

  --h6: calc(var(--base) * pow(var(--scale), 0));
  --h5: calc(var(--base) * pow(var(--scale), 1));
  --h4: calc(var(--base) * pow(var(--scale), 2));
  --h3: calc(var(--base) * pow(var(--scale), 3));
  --h2: calc(var(--base) * pow(var(--scale), 4));
  --h1: calc(var(--base) * pow(var(--scale), 5));
}

h1 { font-size: var(--h1); }
h2 { font-size: var(--h2); }
h3 { font-size: var(--h3); }
h4 { font-size: var(--h4); }
h5 { font-size: var(--h5); }
h6 { font-size: var(--h6); }

# data[].attributes.slidedeck.data[].slides[].desc.html
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;
}

h1, h2, h3, h4, h5, h6 {
  font-size: calc(var(--base) * pow(var(--scale), var(--font-factor, 0)));
}

h1 { --font-factor: 5; }
h2 { --font-factor: 4; }
h3 { --font-factor: 3; }
h4 { --font-factor: 2; }
h5 { --font-factor: 1; }

# data[].attributes.slidedeck.data[].slides[].desc.html
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 {
  font-size: calc(var(--base) * pow(var(--scale), var(--font-factor, 0)));
}

h1     { --font-factor: 5; }
h2     { --font-factor: 5; }
h3     { --font-factor: 3; }
h4     { --font-factor: 2; }
h5     { --font-factor: 1; }
.big   { --font-factor: 10; }
.small { --font-factor: -1; }

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

: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; }

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
I know that many of you like fluid type. That wouldn’t work with my system or would it? I mean, it does, but it’s hacky. Thanks to my friend Matthias Ott I found a solution. I went over to utopia.fyi and defined a min and max viewport. Then a min and a max value. The min value is the smallest font factor and the max value the largest.

The problem: You can’t use unit-less values in the clamp function. Utopify gives you rem values.

# data[].attributes.slidedeck.data[].slides[].desc.html
As you can see here in this clamp function I copied from Utopia.

The solution is in the line below I copied from a Stackoverflow answer by Temani Afif (of course). You wrap the clamp function in a atan2 math function that you wrap in a tan math function. That gives you the computed values of the rem values without the px unit.

Don’t ask me what’s happening here and why it works. I have no idea.

# data[].attributes.slidedeck.data[].slides[].desc.html
For it to work you have to register the custom property.

# data[].attributes.slidedeck.data[].slides[].desc.html
That works pretty well, but please be cautious, because you may run into accessibility issues when you zoom the site. Please always test that first before you use fluid type.

# data[].attributes.slidedeck.data[].slides[].desc.html
I’m using that in my personal website and you can see how the h1 one has a really large font size.

I wanted to see what happens…

# data[].attributes.slidedeck.data[].slides[].desc.html
… if I tick the “text only” checkbox in Firefox Josh showed us yesterday.

# data[].attributes.slidedeck.data[].slides[].desc.html
I got this. The type is waaaay too large.

# data[].attributes.slidedeck.data[].slides[].desc.html
I didn’t use Josh’s solution but I did I implemented a super simple fix.

# data[].attributes.slidedeck.data[].slides[].desc.html
I wrapped my value in a min function with maximum value of 97px where the scaling stops.

# data[].attributes.slidedeck.data[].slides[].desc.html
Now you can see how the text is zoomed in, but the font size of the h1 stops at 97px.

# data[].attributes.slidedeck.data[].slides[].desc.html
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…

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

# data[].attributes.slidedeck.data[].slides[].desc.html
… 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.

matuzo.at/fckafd

# data[].attributes.slidedeck.data[].slides[].desc.html
With my new knowledge, I switched to oklch() because it gives me more flexibility and I have access to much more colors.

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

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
With oklch, we now have access to 50% more colors, and all modern screens support that.

# data[].attributes.slidedeck.data[].slides[].desc.html
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-main: oklch(0.5 0.09 130.31);
  --color-neutral: oklch(0.5 0 0);
  --color-contrast: oklch(0.5 0.09 9.76);
  --color-alt-contrast: oklch(0.5 0.11 69.33);
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

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

# data[].attributes.slidedeck.data[].slides[].desc.html
And this gives us this really nice colour scale for each base colour. They look nice, but they’re far from perfect. The really light tints are actually not light, and that’s because…

# data[].attributes.slidedeck.data[].slides[].desc.html
…out of gamut is the wild west.

So, please, browsers, replace gamut clipping with gamut mapping.

# data[].attributes.slidedeck.data[].slides[].desc.html
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]

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

# data[].attributes.slidedeck.data[].slides[].desc.html
That’s why I select the root element and I set —magic to true.

:root {
  --magic: true;
}

# data[].attributes.slidedeck.data[].slides[].desc.html
And now the header looks like a proper header, and the list in the nav element looks like a navigation.

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

@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);
  }
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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-s-magic: true;
  --oli-s-layout: horizontal;
  --oli-s-font-stack: transitional;
}

# data[].attributes.slidedeck.data[].slides[].desc.html
I’m using some of the font stacks from modernfontstacks.com. That’s basically a switch statement in CSS.

:root {
  --_oli-font-stack-humanist: Seravek, "Gill Sans Nova", Ubuntu, Calibri,"DejaVu Sans", source-sans-pro, sans-serif;
  --_oli-font-stack-geometric: Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif;
  --_oli-font-family-old: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif;
  --_oli-font-family-slab-serif: Rockwell, "Rockwell Nova", "Roboto Slab", "DejaVu Serif", "Sitka Small", serif;
  --_oli-font-family-transitional: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif;
  --_oli-font-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);
  }
}

# data[].attributes.slidedeck.data[].slides[].desc.html
[ I showed a video on how I customize settings on the website olicss.com]

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

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

# data[].attributes.slidedeck.data[].slides[].desc.html
Another thing you’ll se in my CSS are these two lines, which allow you to define two main breakpoints.

# data[].attributes.slidedeck.data[].slides[].desc.html
Unfortunately, we can’t use custom properties inside of media queries.

# data[].attributes.slidedeck.data[].slides[].desc.html
Custom media queries would bring us close to what I want to achieve but they only supported in Firefox Nightly behind a flag.

# data[].attributes.slidedeck.data[].slides[].desc.html
Instead I use container style queries. I have two custom properties. One defines the breakpoint. The other tells me if the breakpoint is active. It’s set to a min function that takes two values. The first value is the breakpoint and the second the viewport width. Before I explain the rest, let’s quickly talk about the min function because I keep forgetting how it works.

# data[].attributes.slidedeck.data[].slides[].desc.html
The min function takes at least two values and picks the smallest for me. In this case 10px because it’s smaller than 50px.

# data[].attributes.slidedeck.data[].slides[].desc.html
If we have a value like 100% and it’s smaller than 50px, then 100% wins.

# data[].attributes.slidedeck.data[].slides[].desc.html
If it’s larger, than 50px wins.

# data[].attributes.slidedeck.data[].slides[].desc.html
Going back to my custom media queries, if the viewport width is smaller than 48rem, 100vw wins. That could be 600px or 400px or 200px.

# data[].attributes.slidedeck.data[].slides[].desc.html
If the viewport is larger than 48rem, 48rem wins.

# data[].attributes.slidedeck.data[].slides[].desc.html
In my container style query I check if 48rem equals 48rem, which is the case if the viewport width is larger than 48rem.

# data[].attributes.slidedeck.data[].slides[].desc.html
Finally, for this to work we have to register one of the properties.

# data[].attributes.slidedeck.data[].slides[].desc.html
I didn’t come up with the idea of using custom properties heavily 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.

# data[].attributes.slidedeck.data[].slides[].desc.html
<div class="card">
  <div class="card-inner">
    <div class="card-content">
      <h3 class="card-heading">Demo</h3>
      <p>Content</p>
    </div>

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

# data[].attributes.slidedeck.data[].slides[].desc.html
That’s how it looks like: An image followed by a heading and text.

# data[].attributes.slidedeck.data[].slides[].desc.html
If we want a larger variation of the same component, we set —card-size: l;

<div class="card" style="--card-size: l">
  <div class="card-inner">
    …
  </div>
</div>

# data[].attributes.slidedeck.data[].slides[].desc.html
We can also change the layout by defining another property, —card-axis: x;

<div class="card" style="--card-size: l; --card-axis: x">
  <div class="card-inner">
    …
  </div>
</div>

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

# data[].attributes.slidedeck.data[].slides[].desc.html
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.

.card-custom {
  --card-size: l;
  --card-axis: x;
  --card-scheme: dark
}

# data[].attributes.slidedeck.data[].slides[].desc.html
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" style="--card-size: s">…</div>
  <div class="card" style="--card-size: s">…</div>
  <div class="card" style="--card-size: s">…</div>
  <div class="card" style="--card-size: s">…</div>
</div>

# data[].attributes.slidedeck.data[].slides[].desc.html
…, 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>

# data[].attributes.slidedeck.data[].slides[].desc.html
Everything I showed you is online at olicss.com. It is not reading for production yet because I’m still working on it, and may never be production ready because it’s just my personal playground. Until recently, another reason why it wasn’t production ready was browser support.

# data[].attributes.slidedeck.data[].slides[].desc.html
But that changed like 3 weeks ago when container style queries finally shipped in Firefox, thanks to my friend Jake who implemented them personally after I blamed him publicly for the lack of support last year at Smashing Conf in Freiburg.

# data[].attributes.slidedeck.data[].slides[].desc.html
In order to test how well all I showed you works I took my existing website, removed the CSS and made some minor changes to the markup.

# data[].attributes.slidedeck.data[].slides[].desc.html
Than I changed some of the settings, like the layout, color, paddings, and font weights.

# data[].attributes.slidedeck.data[].slides[].desc.html
…and I got this. That doesn’t look great, but it’s not bad either. With some custom CSS, it looks like this now.

# data[].attributes.slidedeck.data[].slides[].desc.html
…and that’s the new design of my personal website, which I launched just in time for CSSDay so that you can see all the things I presented to you in action. PS: When you visit my site, please update Firefox first.

Alright, that’s it from for today. What you can take away from my talk is to stay curious and check every now and then if it’s maybe a good idea to break a habit.

# data[].attributes.slidedeck.data[].slides[].desc.html
The slides are online. slides: matuzo.at/cssday26

# data[].attributes.slidedeck.data[].slides[].desc.html
Thank you