DevFest Bari presents

BACK<

TOCSS

a Web Standards movie

Pixu

Emiliano Pisu

Father of 2 lovely "monsters"

D&D master, player ... whatever

Senior Design Engineer, Sensei & Co-Host
@ Dev Dojo IT

pixu1980@linkedin linkedin.com/in/pixu1980
pixu1980@github github.com/pixu1980
pixu1980@codepen codepen.io/pixu1980
devdojo@linkedin linkedin.com/company/dev-dojo-it/
devdojo@youtube youtube.com/@devdojo_it
devdojo@twitch twitch.tv/devdojo_it
devdojo@telegram @devdojo_it

In the recent times CSS got more than a powerful level-up

We got nesting, variables, @container queries, @view-transitions, native carousels, scroll snapping, advanced math functions, and many many more...

Some of these new features can bring
logic, state, and data to CSS

Are you nuts?

Logic, state and data?
In CSS?

🔥 Hell yeah 🔥

Let's introduce the following new CSS features...

typed attr() function

This will be the HTML state layer

Get and parse attribute typed values directly inside CSS, using the `attr()` function

custom @function combined with if() function

This will be the CSS logic layer

Allows to implement custom logic within CSS

if() function combined with custom properties

This will be the CSS data layer

Bring branching logic to CSS without helper classes

Combining them all the dynamic duo (HTML + CSS) becomes a declarative and powerful rendering engine

It's "time" to go Back to CSS!

...at 88mph

MONTH
DAY
YEAR
HOUR
MIN

NOV

05

88

1955

8888

AM
PM

06

88

00

88

MONTH
DAY
YEAR
HOUR
MIN

OCT

21

88

1985

8888

AM
PM

01

88

22

88

MONTH
DAY
YEAR
HOUR
MIN

OCT

21

88

1985

8888

AM
PM

01

88

20

88

What is attr()?

Bridges HTML attribute typed values directly in CSS, no JavaScript needed. The DOM becomes the source of truth.

The basic syntax

    
    /* Read any attribute value as a string */
    content: attr(data-label);

    /* Read with a type and fallback */
    width: attr(data-width px, 100px);
    
  

Supported Types

Each type tells CSS how to parse and use the attribute value.

Supported types include: angle, color, custom-ident, image, integer, length, length-percentage, number, percentage, resolution, string, time, transform-function

Numeric types

    
    /* Pure number for calculations */
    --speed: attr(data-speed number, 0);

    /* Length units */
    width: attr(data-width px, 100px);
    padding: attr(data-pad em, 1em);
    gap: attr(data-gap rem, 1rem);

    /* Percentage */
    opacity: attr(data-opacity %, 100%);

    /* Angle for rotations */
    rotate: attr(data-angle deg, 0deg);

    /* Time for animations */
    animation-duration: attr(data-duration s, 1s);
    
  

Token type for keywords

    
    /* Token matches exact strings */
    .card {
      --theme: attr(data-theme token, light);
    }

    /* Use with if() for branching */
    background: if(
      attr(data-theme token) = dark,
      #1a1a2e,
      #ffffff
    );
    
  
    
    <div data-theme="dark">...</div>
    
  

attr() in Calculations

Typed values unlock mathematical operations directly in CSS.

Dynamic sizing

    
    <div class="progress" data-value="75"></div>
    
  
    
    .progress {
      --value: attr(data-value number, 0);

      /* Use in calc() */
      width: calc(var(--value) * 1%);

      /* Combine with clamp() */
      opacity: clamp(0.3, var(--value) / 100, 1);

      /* Build complex expressions */
      background: linear-gradient(
        90deg,
        #00f5ff calc(var(--value) * 1%),
        transparent calc(var(--value) * 1%)
      );
    }
    
  

Trigonometric functions

    
    <div class="dial" data-angle="45"></div>
    
  
    
    .dial {
      --angle: attr(data-angle deg, 0deg);

      /* Rotation */
      rotate: var(--angle);

      /* Position on a circle */
      --x: calc(cos(var(--angle)) * 50px);
      --y: calc(sin(var(--angle)) * 50px);
      translate: var(--x) var(--y);
    }
    
  

HTML as the State Layer

Attributes become your single source of truth. CSS reads, never writes.

Bridging to Custom Properties

Store typed attr() values in variables for reuse across the cascade.

The pattern

    
    .component {
      /* Read once at the component root */
      --speed: attr(data-speed number, 0);
      --energy: attr(data-energy number, 1);
      --theme: attr(data-theme token, light);
    }

    /* Descendants inherit and use */
    .component .gauge {
      width: calc(var(--speed) * 1px);
    }

    .component .glow {
      box-shadow: 0 0 calc(var(--energy) * 10px) cyan;
    }

    .component .label {
      color: if(var(--theme) = dark, white, black);
    }
    
  

Scoped inheritance

    
    <div class="card" data-energy="0.8">
      <header>Low power mode</header>
      <div class="glow"></div>
    </div>

    <div class="card" data-energy="1.6">
      <header>High power mode</header>
      <div class="glow"></div>
    </div>
    
  
Each card reads its own data-energy. Children inherit the scoped value. No prop drilling. No JavaScript.

What is if()?

Conditional logic in CSS. Choose values based on conditions.

The basic syntax

    
      if(
        condition1: condition1-true-value;
        ...
        conditionX: conditionX-true-value;
        else: default-value
      )
    
  

Comparison Operators

Compare numbers, lengths, or match tokens.

Simple comparisons

    
      /* Single <if> */
      color: if(style(--scheme: dark): #eeeeee;)

      background-color: if(media(print): black;)

      margin: if(media(width > 700px): 0 auto;)

      color: if(supports(color: lch(7.1% 60.23 300.16)): lch(7.1% 60.23 300.16);)
    
  

Ternary comparisons

    

      /* <if> with else */
      padding: if(style(--size: "2xl"): 1em; else: 0.25em;)

      color: if(media(print): white; else: black;)

      margin: if(media(width < 700px): 0 auto; else: 20px auto)

      color: if(
        supports(color: lch(7.1% 60.23 300.16)): lch(7.1% 60.23 300.16);
        else: #03045e;
      )

      background-color: if(
        supports(color: lch(75% 0 0)): 3px solid lch(75% 0 0);
        else: 3px solid silver;
      )
    
  

Token/String matching

    
      /* Switch <if>s */
      background: if(
        style(--scheme: ice): linear-gradient(#caf0f8, white, #caf0f8);
        style(--scheme: fire): linear-gradient(#ffc971, white, #ffc971);
        else: none;
      )

      /* <if> within a shorthand */
      border: 3px yellow if(
        style(--color: green): dashed;
        style(--color: yellow): inset;
        else: solid;
      )
    
  

if() meets attr()

Read from HTML, branch in CSS. The ultimate reactive pattern.

Reactive styling

    
    <button data-loading="true">Submit</button>
    
  
    
    button {
      /* Disable pointer when loading */
      pointer-events: if(
        attr(data-loading token) = true: none;
        else: auto;
      );

      /* Show spinner or text */
      opacity: if(
        attr(data-loading token) = true: 0.6;
        else: 1;
      );
    }

    button::after {
      content: if(
        attr(data-loading token) = true: 'Loading...';
        else: ''
      );
    }
    
  

Numeric thresholds

    
    <div class="meter" data-value="85"></div>
    
  
    
    .meter {
      --value: attr(data-value number, 0);

      /* Color by threshold */
      --color: if(
        var(--value) >= 80: #00ff88;
        else: if(
          var(--value) >= 50: #ffaa00,
          else: #ff4444
        )
      );

      background: var(--color);
    }
    
  
Nested if() creates multi-level branching.

if() vs Traditional CSS

Fewer selectors, cleaner code, reactive behavior.

Before: class based switching

    
    .button { background: gray; }
    .button.is-primary { background: blue; }
    .button.is-danger { background: red; }
    .button.is-success { background: green; }
    .button.is-warning { background: orange; }
    .button.is-disabled { opacity: 0.5; }
    .button.is-loading { pointer-events: none; }
    
  
7 selectors, class management in JS, easier to get out of sync

After: attribute driven with if()

    
    .button {
      --variant: attr(data-variant token, default);
      --disabled: attr(data-disabled token, false);
      --loading: attr(data-loading token, false);

      background: if(
        var(--variant) = primary: blue;
        var(--variant) = danger: red;
        var(--variant) = success: green;
        var(--variant) = warning: orange;
        else: gray
      );

      opacity: if(
        var(--disabled) = true: 0.5;
        else: 1
      );
      
      pointer-events: if(
        var(--loading) = true: none;
        else: auto
      );
    }
    
  
1 selector only, all logic in CSS, always in sync with HTML state

Powerful Use Cases

From theming to animations, if() handles it all.

Dynamic theming

    
    :root {
      --theme: attr(data-theme token, light);

      --bg: if(
        var(--theme) = dark: #0a0a0f;
        else: #ffffff
      );
      
      --fg: if(
        var(--theme) = dark: #f0f0f0;
        else: #1a1a1a
      );
      
      --accent: if(
        var(--theme) = dark: #00f5ff;
        else: #0066cc
      );
      
      --surface: if(
        var(--theme) = dark: #1a1a2e;
        else: #f9f9f9
      );
    }

    body {
      background: var(--bg);
      color: var(--fg);
    }
    
  

Responsive breakpoint values

    
    .grid {
      --cols: attr(data-cols number, 1);

      /* Clamp columns based on container */
      grid-template-columns: repeat(
        if(
          var(--cols) > 4: 4;
          var(--cols) < 1: 1;
          else: var(--cols)
        ),
        1fr
      );

      /* Adjust gap */
      gap: if(
        var(--cols) >= 3: 2rem;
        else: 1rem
      );
    }
    
  

Animation control

    
    .notification {
      --priority: attr(data-priority token, normal);

      animation-name: if(
        var(--priority) = urgent: pulse-urgent;
        else: fade-in
      );

      animation-duration: if(
        var(--priority) = urgent: 0.3s;
        else: 0.6s
      );

      animation-iteration-count: if(
        var(--priority) = urgent: 3;
        else: 1
      );
    }
    
  

What is @function?

Define reusable calculations with parameters and return values.

The basic syntax

    
    @function function-name(--param1, --param2: default) {
      @return /* computed value */;
    }
    
  
Like Sass functions, but native to CSS. No build step required.

Parameters and Defaults

Flexible inputs with sensible fallbacks.

Required parameters

    
    /* Simple function with one param */
    @function double(--value) {
      @return calc(var(--value) * 2);
    }

    .box {
      width: double(50px);   /* 100px */
      height: double(3rem);  /* 6rem */
    }
    
  

Default values

    
    /* Spacing function with defaults */
    @function space(--multiplier: 1, --base: 0.5rem) {
      @return calc(var(--multiplier) * var(--base));
    }

    .card {
      padding: space();        /* 0.5rem */
      margin: space(2);        /* 1rem */
      gap: space(4, 0.25rem);  /* 1rem */
    }
    
  

Multiple parameters

    
    /* Fluid sizing with min, preferred, max */
    @function fluid(--min, --preferred, --max) {
      @return clamp(var(--min), var(--preferred), var(--max));
    }

    h1 {
      font-size: fluid(1.5rem, 4vw, 3rem);
    }

    .container {
      width: fluid(300px, 80%, 1200px);
    }
    
  

Math Inside Functions

Combine calc(), clamp(), min(), max() for powerful calculations.

Calculation chains

    
    /* Convert pixels to rem */
    @function px-to-rem(--px, --base: 16) {
      @return calc(var(--px) / var(--base) * 1rem);
    }

    /* Percentage of a value */
    @function percent-of(--value, --percent) {
      @return calc(var(--value) * var(--percent) / 100);
    }

    .element {
      font-size: px-to-rem(18);       /* 1.125rem */
      width: percent-of(400px, 75);   /* 300px */
    }
    
  

Bounded values

    
    /* Glow that scales but stays reasonable */
    @function glow-size(--energy, --base: 8px) {
      @return clamp(4px, calc($energy * $base), 48px);
    }

    /* Grid columns with limits */
    @function grid-cols(--count, --min: 1, --max: 6) {
      @return clamp(var(--min), var(--count), var(--max));
    }

    .panel {
      box-shadow: 0 0 glow-size(1.5) cyan;
    }

    .grid {
      grid-template-columns: repeat(grid-cols(8), 1fr);
      /* Results in 6 columns (clamped) */
    }
    
  

Trigonometric calculations

    
    /* Position on a circle */
    @function circle-x(--angle, --radius) {
      @return calc(cos(var(--angle)) * var(--radius));
    }

    @function circle-y(--angle, --radius) {
      @return calc(sin(var(--angle)) * var(--radius));
    }

    .orbit-item {
      --angle: attr(data-angle deg, 0deg);
      --x: circle-x(var(--angle), 100px);
      --y: circle-y(var(--angle), 100px);
      translate: var(--x) var(--y);
    }
    
  

@function meets if()

Add conditional logic inside your functions.

Conditional returns

    
    /* Color based on value threshold */
    @function status-color(--value) {
      @return if(
        var(--value) >= 80: #00ff88;
        var(--value) >= 50: #ffaa00;
        else: #ff4444
      );
    }

    .meter {
      --value: attr(data-value number, 0);
      background: status-color(var(--value));
    }
    
  

Mode based calculations

    
    /* Spacing that adapts to density */
    @function adaptive-space(--base, --density) {
      @return if(
        var(--density) = compact: calc(var(--base) * 0.5);
        var(--density) = comfortable: var(--base);
        else: calc(var(--base) * 1.5)
      );
    }

    .list {
      --density: attr(data-density token, normal);
      gap: adaptive-space(1rem, var(--density));
    }
    
  
    
    <ul class="list" data-density="compact">...</ul>
    <ul class="list" data-density="comfortable">...</ul>
    
  

Toggle helpers

    
    /* Boolean to value converter */
    @function when-true(--condition, --value, --fallback: initial) {
      @return if(
        var(--condition) = true: var(--value);
        else: var(--fallback)
      );
    }

    @function when-false(--condition, --value, --fallback: initial) {
      @return if(
        var(--condition) = false: var(--value);
        else: var(--fallback)
      );
    }

    .button {
      --loading: attr(data-loading token, false);
      pointer-events: when-true(var(--loading), none, auto);
      opacity: when-true(var(--loading), 0.6, 1);
    }
    
  

Composing Functions

Functions can call other functions. Build complex logic from simple parts.

Nesting functions

    
    /* Base conversion */
    @function to-rem(--px) {
      @return calc(var(--px) / 16 * 1rem);
    }

    /* Fluid sizing using to-rem */
    @function fluid-text(--min-px, --max-px) {
      @return clamp(
        to-rem(var(--min-px)),
        calc(1rem + 2vw),
        to-rem(var(--max-px))
      );
    }

    h1 { font-size: fluid-text(24, 48); }
    h2 { font-size: fluid-text(20, 36); }
    p  { font-size: fluid-text(16, 20); }
    
  

Building design systems

    
    /* Base spacing unit */
    @function space(--n: 1) {
      @return calc(var(--n) * 0.25rem);
    }

    /* Semantic spacing */
    @function gap-sm() { @return space(2); }   /* 0.5rem */
    @function gap-md() { @return space(4); }   /* 1rem */
    @function gap-lg() { @return space(8); }   /* 2rem */
    @function gap-xl() { @return space(16); }  /* 4rem */

    /* Usage */
    .card {
      padding: gap-md();
      margin-bottom: gap-lg();
    }

    .stack {
      gap: gap-sm();
    }
    
  

Real world: shadow system

    
    @function shadow-color(--alpha: 0.1) {
      @return rgba(0, 0, 0, var(--alpha));
    }

    @function shadow(--elevation: 1) {
      @return
        0 calc(var(--elevation) * 1px) calc(var(--elevation) * 2px) shadow-color(0.1),
        0 calc(var(--elevation) * 2px) calc(var(--elevation) * 4px) shadow-color(0.05);
    }

    .card { box-shadow: shadow(2); }
    .modal { box-shadow: shadow(8); }
    .dropdown { box-shadow: shadow(4); }
    
  

Powerful Use Cases

From design tokens to animations, functions enable reusable patterns.

Color manipulation

    
    /* Lighten/darken with oklch */
    @function lighten(--color, --amount) {
      @return oklch(from var(--color) calc(l + var(--amount)) c h);
    }

    @function darken(--color, --amount) {
      @return oklch(from var(--color) calc(l - var(--amount)) c h);
    }

    @function alpha(--color, --opacity) {
      @return oklch(from var(--color) l c h / var(--opacity));
    }

    .button {
      --base: #0066cc;
      background: var(--base);
    }

    .button:hover {
      background: lighten(var(--base), 0.1);
    }

    .button:active {
      background: darken(var(--base), 0.1);
    }
    
  

Animation helpers

    
    /* Stagger delay based on index */
    @function stagger(--index, --base: 50ms) {
      @return calc(var(--index) * var(--base));
    }

    /* Easing curves */
    @function ease-out-expo() {
      @return cubic-bezier(0.16, 1, 0.3, 1);
    }

    .list-item {
      --index: attr(data-index number, 0);
      animation: fade-in 0.4s ease-out-expo();
      animation-delay: stagger(var(--index));
    }
    
  

Layout calculations

    
    /* Container width with max */
    @function container(--max: 1200px, --padding: 1rem) {
      @return min(calc(100% - var(--padding) * 2), var(--max));
    }

    /* Aspect ratio box height */
    @function aspect-height(--width, --ratio) {
      @return calc(var(--width) / var(--ratio));
    }

    .wrapper {
      width: container();
      margin-inline: auto;
    }

    .video {
      height: aspect-height(100%, 1.777); /* 16:9 */
    }
    
  

Case Studies

So how can we put all this sh... ehm... stuff together?

The Flux Capacitor

What makes time travel possible

Pure CSS recreation of the iconic device. No images, No JavaScript, just gradients, masks, and keyframe animations working in harmony. See this on Codepen
Disconnect Capacitor Drive
Before Opening
Shield eyes from light

Case Study: Fretboard

Custom elements + typed attr() = dynamic chord diagrams

See this on Codepen
        
          <fret-board frets="4" strings="6" chord="E Major">
            <string-note string="6" open></string-note>
            <string-note string="5" fret="2" finger="2"></string-note>
            <string-note string="4" fret="2" finger="3"></string-note>
            <string-note string="3" fret="1" finger="1"></string-note>
            <string-note string="2" open></string-note>
            <string-note string="1" open></string-note>
          </fret-board>
        
      

Fretboard CSS: Typed attr() Magic

Grid from Attributes
        
          /* fret board definition */
          fret-board {
            --fb--frets: 
              attr(frets integer, 4);
              
            --fb--strings: 
              attr(strings integer, 6);

            display: grid;
            grid-template-columns: repeat(var(--fb--frets), 1fr);
            grid-template-rows: repeat(var(--fb--strings), 1fr);
          }
        
      
Note Positioning
        
          /* single note position CSS */
          string-note {
            --fb--fret: 
              attr(fret integer, 0);
              
            --fb--string: 
              attr(string integer, 0);

            grid-column: var(--fb--fret);
            grid-row: var(--fb--string);
            /* this sets the note positions */
          }
        
      

Fretboard Variations

Change attributes, watch it adapt

Case study lessons learned

Three releases, three truths about attribute driven CSS.
  1. Contracts win
    Document data-* invariants and Baseline status next to them. Future teams thank you.
  2. CSS is logic
    attr(), if(), and @function handled 90% of branching. JS only mirrored state.
  3. Accessibility improves
    The same attributes power live regions, toggles, and telemetry strings. Less drift, more clarity.

Wrapping up

HTML attributes become the state layer. CSS attr(), if(), and @function deliver the logic. Together they form a reactive system that flows through the cascade.

Pros

  • Single source of truth in markup
  • No runtime JS for state changes
  • Debuggable in DevTools panel
  • Zero bundle for UI reactivity
  • Progressive enhancement
  • Framework agnostic architecture

Cons

  • Browser support still incomplete
  • Tooling and linter gaps
  • Limited logic expressiveness
  • Requires @supports fallbacks
  • Learning curve for CSS newbies
  • Potential debugging issues

Conclusions

The leap from JavaScript driven UI to CSS driven UI is already happening. Start small: pick one component, move state to attributes, and watch the cascade handle the rest.

As the flux capacitor powers the time travel, your stylesheets can power the interface.

The UI jumps at 88 mph.

Slides Links

Slides
Slides
Slides Repo on Github
Slides Repo

Personal Links

An Awesome CSS Link a Day
An Awesome CSS Link a Day
pixu1980 @ linkedIn
pixu1980 @ linkedIn
© 1987-2025 - Warner Bros. Entertainment Inc.
Pixu

Grazie ❤️!

Dev Dojo IT Logo