John Otander

Components, props, and Cartesian products

Components are the building blocks of a design system, and their public API is expressed as props. Consider a Button component and what its props API might look like.

It will likely have variants like primary, secondary, and link, and states like loading, disabled, and error. In addition to those props APIs there could be other props exposed including size.

Consulting the documentation

The best kind of component is a documented component, so a Button’s docs will often start off with some type of example usage for a component.

<Button variant="primary" size="lg">
  Hello, world!
</Button>

Example usage is typically accompanies by a table which contains the possible values for the props:

PropValues
variantprimary, secondary, link
stateloading, error, disabled
sizesm, md, lg

This is super useful for developers. However, this isn’t very visual. How do these props affect the button’s rendering?

Rendering prop combinations

In order to visualize all possible props combinations, one can copy a component usage with the new props applied. This is a great way to survey all possible outcomes of the button’s appearance. If you wanted to look at all sizes we might copy the JSX code (or manually apply design tool symbols to side by side buttons):

<h2>Button size</h2>
<Button size="s">Hello, world!</Button>
<Button size="m">Hello, world!</Button>
<Button size="l">Hello, world!</Button>

Not bad. Users can quickly see the different sizes available and you get the benefit of seeing them in relation to each other. However, this is manually doing tasks that the computer is good at. Computation.

If you surface all props and their values (like above in our props table), you can compute all possible button variations. This also ensures that all button specimens are displayed when props and their possible values are added and removed.

Otherwise, If you add another size, you need to manually update the documentation. You’ll also notice, that with only three props with three possible values each, there are a lot of possible values.

There are 27 variations to be exact.

The Cartesian product

In mathematics, there is the Cartesian product, which lends itself nicely to displaying all possible props combinations. The Cartesian product of two or more sets, is the resulting set of all ordered pairs.

Ordered pairs

The ordered pair is an important distinction because we want the permutation of all possible props.

With combinations, order doesn’t matter, so 1234 and 4321 represent the same value. With permutations, the order matters just like it would in a keypad. If your phone’s unlock code is 1234, typing in 4321 wouldn’t work because order is significant.

This order property in Cartesian products is advantageous because we don’t want the same prop combinations with different ordering. Consider JSX, the following two code snippets will render the same thing:

  • <Button size="lg" variant="primary" />
  • <Button variant="primary" size="lg" />

Calculating the Cartesian product

The Cartesian product between sets A and B looks like the following in mathematical notation:

A × B = { (a, b) | a ∈ A and b ∈ B}

It looks a bit intimidating, but it essentially means that if A is represented as rows in a table, while B is columns, you can combine them. All new table cells are part of the resulting set, the Cartesian product.

variant × sizesmmdlg
primaryprimary, smprimary, mdprimary, lg
secondarysecondary, smsecondary, mdsecondary, lg
linklink, sm,link, mdlink, lg

Which can then be represented as JSX code with the Button component:

  • <Button variant="primary" size="sm" />
  • <Button variant="primary" size="md" />
  • <Button variant="primary" size="lg" />
  • <Button variant="secondary" size="sm" />
  • <Button variant="secondary" size="md" />
  • <Button variant="secondary" size="lg" />
  • <Button variant="link" size="sm" />
  • <Button variant="link" size="md" />
  • <Button variant="link" size="lg" />

Building a Cartesian component

First, consider what an ergonomic API might look like for a Cartesian component. It’d need to accept the component, and then a collection of props with their possible values.

import React from 'react'
import Cartesian from '../src/design-system/cartesian'
import Button from '../src/button'

export default () => (
  <Cartesian
    component={Button}
    variant={['primary', 'secondary', 'link']}
    state={['loading', 'error', 'disabled']}
    size={['sm', 'md', 'lg']}
  />
)

An implementation would look something similar to (Cartesian product implementation is left out for brevity):

import React, { Fragment } from 'react'
import { cartesianProduct } from './util'

export default ({ component, container = Fragment, ...props }) => {
  const combinations = cartesianProduct(props)
  const Component = component
  const Container = container

  return (
    <Fragment>
      {combinations.map((props, i) => (
        <Container key={i}>
          <Component {...props} />
        </Container>
      ))}
    </Fragment>
  )
}

This would render the component for every prop combination produced by cartesianProduct.

Usage of Cartesian component

Lastly, imagine if in your documentation the possible props were statically extracted from the component’s source code. You can then forward it along to the Cartesian component as part of some docs generation step.

// ...
export default (props) => <Cartesian component={Button} {...props.propTypes} />

When the prop types in the Button’s source code change, the resulting Cartesian product documentation would be automatically updated as well.

Revealing other UI states

Computing a Cartesian product doesn’t need to be limited to props, either. Other states, like hover and focus styles, can be computed to provide an all in one overview of a component and its interactive aspects.

Shout out to Derrick Marcey for pointing this out on Twitter.

Conclusion

There are a lot of interesting things we can do when we start thinking about computational design and generative documentation. The Cartesian component is a pretty simple, yet powerful, usage of this new way of thinking.

In fact, we can automate displaying component variations so we can better diagnose what combinations cause issues and better documenting aspects of a component’s implementation.

Another interesting usage of the Cartesian product is being able to quickly select a component variation from all possibilities when working inside a design or development tool.


Sign up for my newsletter

If you want early access to what I'm researching, writing, and building, you should subscribe to my newsletter.

No spam, ever. You can unsubscribe at any time.