Crafting Stateful Styles

@davidkpiano · dotCSS 2019

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 · DotCSS 2019

Thank you dotCSS!

Crafting Stateful Styles

By David Khourshid

Crafting Stateful Styles

DotCSS

  • 7,039