Engineering CSS
From type to function
Some people tell me...
Some people tell me
CSS is NOT a programming language
And I'm not a real programmer :(
(aaaawwwww)
In order for it to be a programming language...
It should be able to round things
.round {
--interval: 25px;
height: round(nearest, var(--input), var(--interval));
}
It should have booleans
.demo-state {
--set-border-radius: false;
--border-radius: 0;
}
:root:has(.checkbox:checked) {
--set-border-radius: true;
}
@container style(--set-border-radius: true) {
.demo-stage {
--border-radius: 10px;
}
}
It should be able to do difficult maths
:root {
--indigo: oklch(56.6% 0.27 170);
--c-base: 0.05;
--indigo-10: oklch(
from var(--indigo) 10% calc(var(--c-base) + (sin(1 * pi) * c)) h
);
}
Yes! but it's still not a programming language!
You can't even type CSS
@property --move-x {
syntax: "<percentage>";
initial-value: 0%;
inherits: false;
}
@property --btn-color {
syntax: "<color>";
initial-value: #000;
inherits: true;
}
And even so...
It is the worst language because
It doesn't even have scope
@scope (.article-body) {
img {
border: 5px solid hotpink;
background-color: #c0ffee;
}
}
Layout language?
Layout Engineering language?
CSS :has() evolved as a language
And this is just an intro...
Engineering CSS
(from type to function)
Hi, I'm Brecht
Front-end developer / DevRel
@utilitybend.com

I also write for...

Part of W3C (community) groups
- Open UI
- CSS-Next (CSS4 & 5)
- WHATNOT
Custom Properties
(aka. CSS variables)

Good numbers
State of CSS survey of 2023 states that 93% of survey respondents use custom properties.
- 93% Used it
- 5.7% Heard of it
- 1.3% Never heard of it
So everything is fine right?
Only as a constant
The fundamentals of engineering CSS

More than a constant
Custom properties can
- be overwritten
- have a fallback
- contain anything(!) you want
- or.. can be typed (recently)
- be controlled by JS
The button case
Overwriting and fallbacking
<button>I am a button</button>
<button class="secondary">I am a secondary button</button>
<button class="outline">I am an outline button</button>
<button class="outline secondary">I am a an outline secondary button</button>
Some default styles
Not important for this demo
button {
padding: 0.813rem 1.25rem;
border-radius: 0.313rem;
font-size: 1.1rem;
cursor: pointer;
/* lazy transition ;) */
transition: all 0.2s;
}

Start engineering
Focus on what these buttons have in common
Filled buttons
Setting it up
button {
--color: white;
background: var(--color);
border: 2px solid var(--color);
color: black;
}
button:is(:hover, :focus) {
--color: aquamarine;
}
button.secondary {
--color: lightgreen;
}
button.secondary:is(:hover, :focus) {
--color: lawngreen;
}
Work with a "private variable"
button {
--_color: var(--color, white);
background: var(--_color);
border: 2px solid var(--_color);
color: black;
}
Work with a "private variable"
button {
--_color: var(--color, white);
background: var(--_color);
border: 2px solid var(--_color);
color: black;
}
button:is(:hover, :focus) {
--color: aquamarine;
}
button.secondary {
--color: lightgreen;
}
button.secondary:is(:hover, :focus) {
--color: lawngreen;
}
Outline buttons
button.outline {
background: transparent;
color: var(--_color);
}
Full code CSS
button {
--_color: var(--color, white);
padding: 0.813rem 1.25rem;
border-radius: 0.313rem;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
background: var(--_color);
border: 2px solid var(--_color);
color: black;
}
button:is(:hover, :focus) {
--color: aquamarine;
}
button.secondary {
--color: lightgreen;
}
button.secondary:is(:hover, :focus) {
--color: lawngreen;
}
button.outline {
background: transparent;
color: var(--_color);
}
Modern approach
The answer is blowing in the wind?
<button class="px-5 py-3 rounded-md text-lg cursor-pointer transition duration-200 bg-white border-2 border-white text-black hover:bg-aquamarine hover:border-aquamarine focus:bg-aquamarine focus:border-aquamarine">I am a button</button>
<button class="px-5 py-3 rounded-md text-lg cursor-pointer transition duration-200 bg-lightgreen border-2 border-lightgreen text-white hover:bg-lawngreen hover:border-lawngreen focus:bg-lawngreen focus:border-lawngreen">I am a secondary button</button>
<button class="px-5 py-3 rounded-md text-lg cursor-pointer transition duration-200 bg-transparent border-2 border-white text-white hover:border-aquamarine hover:text-aquamarine focus:border-aquamarine focus:text-aquamarine">I am an outline button</button>
<button class="px-5 py-3 rounded-md text-lg cursor-pointer transition duration-200 bg-transparent border-2 border-lightgreen text-lightgreen hover:bg-transparent hover:border-lawngreen hover:text-lawngreen focus:bg-transparent focus:border-lawngreen focus:text-lawngreen">I am an outline secondary button</button>

Output CSS
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.813rem;
padding-bottom: 0.813rem;
}
.rounded-md {
border-radius: 0.375rem; /* Tailwind CSS default for rounded-md */
}
.text-lg {
font-size: 1.125rem; /* Tailwind CSS default for text-lg */
}
.cursor-pointer {
cursor: pointer;
}
.transition {
transition-property: background-color, border-color, color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-200 {
transition-duration: 200ms;
}
.bg-white {
background-color: #fff;
}
.border-2 {
border-width: 2px;
}
.border-white {
border-color: #000;
}
Output CSS page 2
.hover\:bg-aquamarine:hover,
.focus\:bg-aquamarine:focus {
background-color: aquamarine;
}
.hover\:border-aquamarine:hover,
.focus\:border-aquamarine:focus {
border-color: aquamarine;
}
.bg-lightgreen {
background-color: lightgreen;
}
.border-lightgreen {
border-color: lightgreen;
}
.hover\:bg-lawngreen:hover,
.focus\:bg-lawngreen:focus {
background-color: lawngreen;
}
.hover\:border-lawngreen:hover,
.focus\:border-lawngreen:focus {
border-color: lawngreen;
}
.bg-transparent {
background-color: transparent;
}
.text-black {
color: #000;
}
.text-white {
color: #fff;
}
.hover\:border-aquamarine:hover,
.focus\:border-aquamarine:focus {
border-color: aquamarine;
}
.hover\:text-aquamarine:hover,
.focus\:text-aquamarine:focus {
color: aquamarine;
}
Output CSS page 3
.bg-lightgreen {
background-color: lightgreen;
}
.border-lightgreen {
border-color: lightgreen;
}
.hover\:bg-lawngreen:hover,
.focus\:bg-lawngreen:focus {
background-color: lawngreen;
}
.hover\:border-lawngreen:hover,
.focus\:border-lawngreen:focus {
border-color: lawngreen;
}
.bg-transparent {
background-color: transparent;
}
.hover\:border-aquamarine:hover,
.focus\:border-aquamarine:focus {
border-color: aquamarine;
}
.hover\:text-aquamarine:hover,
.focus\:text-aquamarine:focus {
color: aquamarine;
}
.text-lightgreen {
color: lightgreen;
}
.hover\:border-lawngreen:hover,
.focus\:border-lawngreen:focus {
border-color: lawngreen;
}
Output CSS page 4
.hover\:text-lawngreen:hover,
.focus\:text-lawngreen:focus {
color: lawngreen;
}
.hover\:bg-transparent:hover,
.focus\:bg-transparent:focus {
background-color: transparent;
}

So we ended with...
button {
--_color: var(--color, white);
padding: 0.813rem 1.25rem;
border-radius: 0.313rem;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
background: var(--_color);
border: 2px solid var(--_color);
color: black;
}
button:is(:hover, :focus) {
--color: aquamarine;
}
button.secondary {
--color: lightgreen;
}
button.secondary:is(:hover, :focus) {
--color: lawngreen;
}
button.outline {
background: transparent;
color: var(--_color);
}
Can we make this even smaller? 😈
(Spoiler, yes we can)
Introduce a second custom property
button {
--_color: var(--color, white);
/* defaults */
background: var(--_color);
border: 2px solid var(--_color);
color: black;
}
button.secondary {
--color: lightgreen;
--hoverColor: lawngreen;
}
/* ... outline ... */
button:is(:hover, :focus) {
--color: var(--hoverColor, aquamarine);
}
CSS Nesting 4 buttons - 22 lines (90% support)
button {
--_color: var(--color, white);
background: var(--_color);
border: 2px solid var(--_color);
color: black;
padding: 13px 20px;
border-radius: 5px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
&.secondary {
--color: lightgreen;
--hoverColor: lawngreen;
}
&.outline {
background: transparent;
color: var(--_color);
}
&:is(:hover, :focus) {
--color: var(--hoverColor, aquamarine);
}
}
Adding an orange and orange outline button
<button class="orange">orange button</button>
<button class="orange outline">outline orange button</button>
&.secondary { /* ... */ }
&.orange {
--color: orange;
--hoverColor: lightsalmon;
}
&.outline { /* ... */ }

Now that's a smart system!
We re-created four buttons
- 22 lines of code
- smart
- easy to manage
- easy to expand
- Use @layer or @scope for specificity issues
Overengineering works!
Engineering grids
Smart controlled grids
You could do this...
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
<!-- Your grid items go here -->
</div>
Let's create a grid
<div class="grid">
<!-- items here -->
</div>
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 3vmax;
}
Automate responsiveness
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 3vmax;
}
Automate responsiveness
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(450px, 100%), 1fr));
gap: 3vmax;
}
Engineering for understanding
Create levers
.grid {
--grid-min: 450px;
--grid-gap: 3vmax;
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--grid-min), 100%), 1fr)
);
gap: var(--grid-gap);
}
Turning into a system
@layer layout {
.grid {
--grid-min: 450px;
--grid-gap: 3vmax;
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--grid-min), 100%), 1fr)
);
gap: var(--grid-gap);
}
}
Design system base
@layer theme {
:root {
--default-column-min: 450px;
--default-gap: 3vmax;
}
}
Private variables
@layer theme {
:root {
--default-column-min: 450px;
--default-gap: 3vmax;
}
}
@layer layout {
.grid {
--_grid-min: var(--column-min, var(--default-column-min));
--_grid-gap: var(--gap, var(--default-gap));
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--_grid-min), 100%), 1fr)
);
gap: var(--_grid-gap);
}
}
Changing props of other grid
<div class="grid product-grid">
<!-- items here -->
</div>
@layer products {
.product-grid {
--column-min: 100px;
--gap: 30px;
}
}
Changing default based on media
@layer theme {
:root {
--default-column-min: 450px;
--default-gap: 3vmax;
@media (width > 1250px) {
--default-gap: 5vw;
}
}
}
Do not stop there!
Add a Flexbox equivalent
@layer layout {
/* Previous grid */
.flex {
--_flex-min: var(--column-min, var(--default-column-min));
--_flex-gap: var(--gap, var(--default-gap));
display: flex;
flex-wrap: wrap;
gap: var(--_flex-gap);
> * {
flex: 1 1 var(--_flex-min);
}
}
}
Change grid to flex
<div class="grid">
<!-- items here -->
</div>
<div class="flex">
<!-- items here -->
</div>
Go beyond with named columns
Going advanced
/* Part of layout-breakouts-builder.vercel.app */
.grid {
display: grid;
grid-template-columns:
[full-start]
var(--full)
[popout-start]
var(--popout)
[content-start]
var(--content)
[inset-content-start]
var(--inset-content)
[inset-content-end]
var(--content)
[content-end]
var(--popout)
[popout-end]
var(--full)
[full-end];
}

Work smarter
You got this!
- Less code output
- Easy maintainable and upgradable
- More than copying a pretty picture of a website
Danger around the corner...

That one person...
- That thinks #333 is a perfect value for --column-min
- And left that in a 70 files changed pull request
- If only we could debug
@property
Typing custom properties

Using @property
- Part of the CSS Houdini umbrella of APIs
- Allows developers to explicitly define their CSS custom properties
- type checking and constraining
- setting default values
- defining whether a custom property can inherit values or not
- Almost at 93% support
Basic syntax
@property --property-name {
syntax: "<color>";
inherits: false;
initial-value: #c0ffee;
}

Syntax
- length
- number
- percentage
- length-percentage
- color
- image
- url
- integer
- angle
- resolution
- custom-ident
- transform-list
Basic demo
@property --color {
syntax: "<color>";
inherits: true;
initial-value: deeppink;
}
div {
background: var(--color);
}
.color-1 { --color: #1a535c; }
.color-2 { --color: rgb(100, 200, 0); }
.color-3 { --color: oklch(90% 0.5 200); }
Making mistakes
.color-1 { --color: 1; }
Let's dive a bit deeper
Listing options
@property --color {
syntax: "aquamarine | cyan | blue";
inherits: true;
initial-value: blue;
}
Also possible for mixed values
@property --sometime {
syntax: "<time> | <integer>";
inherits: true;
initial-value: 1s;
}
Also possible for mixed values
@property --my-position {
syntax: "<length> | auto";
inherits: true;
initial-value: auto;
}
Multiple values
# supports a comma separated list
@property --background {
syntax: "<image>#";
inherits: true;
initial-value:
linear-gradient(
to right in oklab,
oklch(70% 0.5 340) 0%, oklch(90% 0.5 200) 100%
), radial-gradient(
farthest-corner circle at 100% 0% in oklab,
oklch(80% .4 222) 0%, oklch(35% .5 313) 100%
);
}
.item {
background-image: var(--background);
}
Multiple values
+ supports a space separated list
@property --box-shadow-length {
syntax: "<length>+";
inherits: false;
initial-value: 0px 0px 3px 1px;
}
.item {
box-shadow: var(--box-shadow-length) #000;
}
Limitation
Initial value must be supported by the browser (use @supports)
Typing can help the browser
Setting up our image to animate on scroll
:root {
--move-x: 0%;
--move-y: 0%;
--scale: 20%;
}
body {
/* to create a scroll, demo purpose */
height: 400vh;
}
img {
/* positioning props*/
clip-path: circle(var(--scale) at var(--move-x) var(--move-y));
animation: rotateOrb linear both;
animation-timeline: scroll();
}
Adding an animation for our custom properties
@keyframes rotateOrb {
0% {
--move-x: 0%;
--move-y: 0%;
}
25% {
--move-x: 100%;
--move-y: 0%;
}
50% {
--move-x: 100%;
--move-y: 100%;
}
75% {
--move-x: 0%;
--move-y: 100%;
--scale: 60%;
}
100% {
--move-x: 0%;
--move-y: 0%;
--scale: 150%;
}
}
Adding @property in the mix
@property --move-x {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
@property --move-y {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
@property --scale {
syntax: "<percentage>";
inherits: false;
initial-value: 20%;
}
/* remove the :root declaration now */
This opens up a window of possibilities
Hue rotation
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes spin {
0% {
--angle: 0deg;
}
100% {
--angle: 360deg;
}
}
div::after {
background: hsl(var(--angle) 90% 50%);
animation: spin 5.5s linear infinite;
}
Hue rotation
/* Star properties */
@property --star-offset {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
@property --star-opacity {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@property --star-transform {
syntax: "<transform-function>+ | none";
inherits: true;
initial-value: none;
}
Oh, and one more thing!
An initial value can have a calc() function
@property --scale {
syntax: "<percentage>";
inherits: false;
initial-value: calc(100vh - 20%);
}
So what about functions...?
Functions
Pushing to the limit
Fluid Typography
clamp() (96%)
clamp(min, preferred, max);
clamp() (95.9%)
:root {
--title-size: clamp(1rem, 0.3043rem + 3.4783vw, 3.5rem);
}
h1 {
font-size: var(--title-size);
line-height: 1.3;
}
Dude, subpixels are bad!
clamp() and round() (90%)
:root {
--title-size: clamp(1rem, 0.3043rem + 3.4783vw, 3.5rem);
}
h1 {
--rounding-interval: 1px;
font-size: round(nearest, var(--title-size), var(--rounding-interval));
line-height: 1.3;
}
/* round(up|down|nearest|to-zero, input, interval) */
Color functions
As seen by Matthias Ott
:root {
--primary: oklch(56.6% 0.27 274);
--primary-10: oklch(from var(--primary) 10% c h);
--primary-20: oklch(from var(--primary) 20% c h);
--primary-30: oklch(from var(--primary) 30% c h);
--primary-40: oklch(from var(--primary) 40% c h);
--primary-50: oklch(from var(--primary) 50% c h);
--primary-60: oklch(from var(--primary) 60% c h);
--primary-70: oklch(from var(--primary) 70% c h);
--primary-80: oklch(from var(--primary) 80% c h);
--primary-90: oklch(from var(--primary) 90% c h);
--primary-100: oklch(from var(--primary) 100% c h);
}
Trigonometric functions
sin(), cos(), tan(), asin(), acos(), atan(), atan2()
Updating the Chroma (intensity)
:root {
--primary: oklch(56.6% 0.27 270);
--c-base: 0.05;
--primary-10: oklch(
from var(--primary) 10% calc(var(--c-base) + (sin(1 * pi) * c)) h
);
--primary-20: oklch(
from var(--primary) 20% calc(var(--c-base) + (sin(0.9 * pi) * c)) h
);
--primary-30: oklch(
from var(--primary) 30% calc(var(--c-base) + (sin(0.8 * pi) * c)) h
);
--primary-40: oklch(
from var(--primary) 40% calc(var(--c-base) + (sin(0.7 * pi) * c)) h
);
/* ... */
}
Remember
We're only getting started...
CSSWG
Resolved to adopt
CSS Functions
@function --negative (--value) {
result: calc(-1 * var(--value));
}
div { margin: --negative(var(--gap)); }
CSS Mixins
@mixin --button (--face, --text, --radius) {
--background: var(--face, teal);
--color: color-mix(in lch, var(--text, white) 85%, var(--background));
--border-color: color-mix(in lch, var(--text, white) 80%, var(--background));
@result {
background: var(--background);
border: medium double var(--border-color);
border-radius: var(--radius, 3px);
color: var(--color);
padding: 0.25lh 2ch;
}
}
button[type='submit'] { @apply --button(rebeccaPurple); }
button.danger { @apply --button(maroon); }
CSS conditionals
.item {
background-color: if(
style(--variant: success) ? var(--color-success) :
style(--variant: danger) ? var(--color-danger) :
/* (other variants) */
var( --color-neutral-95)
);
}
CSS Engineering
CSS Engineering is
not a gimmick
CSS Engineering is
smart
CSS Engineering is
performant
CSS Engineering is