Skip to content

Clarify state management in CSS/Style functions #2

@MadeByMike

Description

@MadeByMike

A little "rant" (hopefully a good one) about UI state management... which is different to application state in container components.

In my opinion this example is not fantastic CSS/CSS-in-JS:

const getButtonStyles = ({ isPressed, tokens }) => ({
  background: isPressed ? 'blue' : 'white',
  color: !isPressed ? 'blue' : 'white',
});

export const Button = ({ children, ...props }) => {
  const { isPressed, events } = useButton(props);
  const styles = getButtonStyles({ isPressed });
  return (
    <button {...props} {...events}>
      {children}
    </button>
  );
};

My apologies to whoever wrote this example :) I don't mean to pick on it. It's a pretty typical example I've see on a lot of projects. I want to describe a few problems I've had with this approach as projects get larger.

In examples like this, the value of individual CSS properties depends on the resolution of state within a style function. I'm slowly solidifying my opinion that this is a code-smell for CSS-in-JS.

It's great that the props are resolved down to sensible descriptive flags like isPressed before handing off to he style function. That helps. But as it scales it will become impossible to know the number of different states or variations a UI component has.

As a front-end developer knowing the number of variations a UI component has and what CSS is applied in each case is about 90% of the job. And I want to know this quickly when traversing large projects.

The way to solve this is to map out a set of finite state categories like this:

Modifiers Behavioural Pseudo
Large Pressed focus
Small Disabled hover

You should only have one Modifier and one Behaviour active at any given time. If you find 2 behaviours can be active at the it's usually an indication that this component could be two components but you can add another state category if needed.

This makes it possible to know the number of UI states a component can have 2 x 2 x 2 = 8. Suddenly we can validate this against the design. With the props resolved against individual CSS properties it's not possible to know or test that the resolution of the style function results in something valid and intended.

The next part is to make these styles more 'ergonomic'. We want to know quickly what the small + pressed variation is without resolving everything in our head. If I can't resolve UI state to set to applied styles in under 5 sec it makes me sad. Not making me sad should be a primary goal of a design system.

My solution is this:

const modifiers = {
  'large': {
    fontSize: '2rem'
  },
 'small': {
    fontSize: '0.8rem'
  }
}
const behaviours = {
  'pressed': {
    background: 'blue'
    color: 'white'
  },
 'disabled': {
    color: '#888'
  }
}

const getButtonStyles = ({ modifier, behaviour }) => ({
  background: 'white',
  color: 'blue',
  ...modifiers[modifier]
  ...behaviours[behaviour]
});

export const Button = ({ children, ...props }) => {
  const { modifier, behaviour, events } = useButton(props);
  const styles = getButtonStyles({ modifier, behaviour });
  return (
    <button css={styles} {...props} {...events}>
      {children}
    </button>
  );
};

(I've modified it so that the useButton function resolves the modifier and behaviour states)

If a modifier changes the value of a behaviour we can use CSS properties:

const modifiers = {
  'large': {
    fontSize: '2rem'
    '--pressedBackground': 'red'
  }
}
const behaviours = {
  'pressed': {
    background: 'val(--pressedBackground, 'blue')'
  }
}

Now for the large variation only the pressed state will be red/white rather than the default blue/white.

I'm not fixed on my suggested implementation but rather the goals of:

  • Knowing how many UI states a can component has
  • Ensuring these states can be easily validated against the design
  • Ensuring styles are ergonomic and easy to read

You'll notice I kinda borrow naming conventions here from BEM (modifier/behaviour) I have other ideas about how to communicate all intentions and give semantic meaning to styles\components with large systems and I think this is pretty important too. -- Another issue.

Thank you for coming to my TED talk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions