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

Me, Brecht

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

for you

CSS Engineering is

programming