From DHTML
to CSS Variables

@davidkpiano · Flashback Conference 2020

setInterval(move, 25)

requestAnimationFrame(move)

no idea

60 FPS

2012 →

document.body.appendChild(o)

var

let

const


<DIV ID="oDiv"
  STYLE="background-color: #CFCFCF; position: absolute; 
         left: expression(document.body.clientWidth / 2 - oDiv.offsetWidth / 2);
         top: expression(document.body.clientHeight / 2 - oDiv.offsetHeight / 2)">
  Example DIV
</DIV>

CSS Expressions (IE7)


<style type="text/javascript">
  tags.H1.color = "red";
  tags.p.fontSize = "20pt";
  with (tags.H3) {
      color = "green";
  }
  with (tags.H2) {
      color = "red";
      fontSize = "16pt";
      marginTop = "4cm";
  }
</style>

JavaScript Style Sheets (Netscape 4)

CSS Custom Properties

--color-primary: blue;
<h1 style="--color-primary: blue;">
  Hello Orlando
</h1>
:root {
  --color-primary: blue;
}
h1 {
  color: var(--color-primary, red);
}
section {
  --color-primary: green;
}

Custom Property

Value

Default

.box {
  --delta-x: 150px;
}

CSS

.box {
  --delta-x: 150px;
  transform: translateX(
    var(--delta-x));
}

CSS

.box {
  --delta-x: 150;
  transform: translateX(
    calc(var(--delta-x) * 1px));
}

CSS

.box {
  transform:
    translateX(calc(var(--delta-x) * 1px));
}

const box = document.querySelector('#box');
const hBox = new Hammer(box);

hBox.on('panleft panright', (e) => {
  box.style
    .setProperty('--delta-x', e.deltaX);
});

hBox.on('panend', () => {
  box.style
    .setProperty('--delta-x', 0);
});

CSS

JAVASCRIPT

.box {
  --not-panning: calc(-1 * var(--panning) + 1);
  transition:
    transform
    calc(var(--not-panning) * .6s)
    ease-out;
  transform:
    translateX(calc(var(--delta-x) * 1px))
    rotate(calc(var(--delta-x) * .1deg));
}

const box = document.querySelector('#box');
const hBox = new Hammer(box);

hBox.on('panleft panright', (e) => {
  box.style
    .setProperty('--delta-x', e.deltaX);
  box.style
    .setProperty('--panning', 1); // true
});

hBox.on('panend', () => {
  box.style
    .setProperty('--delta-x', 0);
  box.style
    .setProperty('--panning', 0); // false
});

CSS

JAVASCRIPT

calc(-1 * var(--panning) + 1)
-1 * 1 + 1 = 0
-1 * 0 + 1 = 1

I warned you there would be lots of demos

CSS

CSSS

State

Fetch data

Fetch data

Fetch data

Fetch data

Normal

Hover

Active

Disabled

Fetched data

Success

Failed to fetch

Error

Fetching data...

Loading

Fetching data...

Loading

<button class="button loading">
  Fetching data...
</button>

I wasn't always good at JavaScript.

input[type="checkbox"] {
  /* ... */
}

input[type="checkbox"] ~ #app {
  /* ... */
}

input[type="checkbox"] ~ #app .box {
  background-color: white;
}

input[type="checkbox"]:checked
  ~ #app .box {
  background-color: blue;
}

~

checkbox

#app

.box

~

Adjacent sibling selector

label for="..."

label for="..."

checkboxes

radios

Zero or more

One at a time

Fetching data...

<button class="loading">
  Fetching data...
</button>

<button class="success">
  Fetched data!
</button>

<button class="loading success">
  Fetched data...?
</button>

Fetched data!

Fetched data...?

Is there a better way to model
state for dynamic UIs?

User flows are transitions

between UI states

due to events

Finite state machines

and statecharts

A state is a dynamic property expressing characteristics of an object that may change in response to user action or automated processes. 

  • aria-busy
  • aria-current
  • aria-disabled
  • aria-grabbed
  • aria-hidden
  • aria-invalid

States do not affect the essential nature of the object, but represent data associated with the object or user interaction possibilities.

:state(...) {
  /* ... */
}
:hover
:invalid
:active
:focus
:empty
:checked

Data Attributes

<button data-state="loading">
  Fetching data...
</button>
button[data-state="loading"] {
  opacity: 0.5;
}
elButton.dataset.state; // 'loading'

elButton.dataset.state = 'success';

delete elButton.dataset.state;

HTML

CSS

JavaScript

<button data-state="success">
  Fetched data!
</button>
<button>
  Fetch data
</button>

Data Attributes

Fetching data...

<button data-state="idle">
  Fetch data
</button>

<button data-state="loading">
  Fetching data...
</button>

<button data-state="success">
  Fetched data!
</button>

<button data-state="error">
  Failed to fetch data
</button>

Fetched data!

Failed to fetch

Fetch data

Loading

Success

Resolve

Initial state

States

Events

Transitions

Final State

Failed

REJECT

RETRY

Finite state machine

Define transitions between
states & events

function searchReducer(state = 'idle', event) {
  switch (state) {
    case 'idle':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'searching':
      switch (event.type) {
        case 'RESOLVE':
          return 'success';
        case 'REJECT':
          return 'failure';
        default:
          return state;
      }
    case 'success':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'failure':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    default:
      return state;
  }
}

FSMs with switch/case

const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};

Define transitions between
states & events

const transition = (state, event) => {
  return machine
    .states[state] // current state
    .on[event]     // next state
    || state;      // or same state
}

FSMs with object mapping

npm install xstate


const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};
import { Machine } from 'xstate';

const machine = Machine({
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
});

idle

dragging

pan

panstart

panend

(assign dx, dy)

(assign x, y)

<main data-state="loading" data-prev-state="idle">

idle

loading

error

<main data-state="loading" data-prev-state="error">
const elApp = document.querySelector('#app');

function setAppState(state) {
  // change data-state attribute
  elApp.dataset.prevState = elApp.dataset.state;
  elApp.dataset.state = state;
  
  // ...
}

✨✨

💥💥💥

[data-state="loading"][data-prev-state="idle"] {
  /* loading initially */
}

[data-state="loading"][data-prev-state="error"] {
  /* loading after error */
}
<main
  data-state="success"
  data-transition="loading success"
>
</main>
[data-transition^="loading"] {
  /* from "loading" */
}

[data-transition="loading success"] {
  /* from "loading" to "success" */
}

HTML

CSS

previous state

current state

start

selecting

selected

dragging

disposed

grabbing

disposed

data-state="idle"

data-show="loading"

data-hide="loading"

data-active

data-active

active when app is in "loading" state

inactive when app is in "loading" state

data-state="loading"

const elApp = document.querySelector('#app');

function setAppState(state) {
  // change data-state attribute
  elApp.dataset.state = state;
  
  // remove any active data-attributes
  document.querySelectorAll(`[data-active]`)
    .forEach(el => {
      delete el.dataset.active;
    });
  
  // add active data-attributes to proper elements
  document
    .querySelectorAll([
      `[data-show~="${state}"]`,
      `[data-hide]:not([data-hide~="${state}"])`
    ].join(','))
    .forEach(el => {
      el.dataset.active = true;
    });
}

// set button state to 'loading'
setAppState('loading');
[data-show~="${state}"]
[data-hide]:not([data-hide~="${state}"])

Think in transitions

not just events,

not just state.

The world of statecharts

Spectrum community

XState Documentation

Make your code do more.

@davidkpiano · Flashback Conference 2020

Thank you Orlando!

From DHTML to CSS Variables

By David Khourshid

From DHTML to CSS Variables

Flashback Conf

  • 4,200