Ad

CSS Module Composition

- 1 answer

I've got a question about composition I'm hoping someone can help me with. I'm using react-css-modules with Sass, and I'd like to know the best way to compose things for one of our basic bottom-level components.

Here's our component:

import React, {PropTypes} from 'react'
import cssModules from 'react-css-modules'
import styles from './style.sass'

const Button = ({children = 'Submit', ...props}) => {
  const align = props.align ? `-${props.align}` : ''
  const type = props.type ? `-${props.type}` : ''
  const styleName = `button${type}${align}`

  return (
    <button onClick={props.onClick} {...props} styleName={styleName}>
      {children}
    </button>
  )
}

Button.propTypes = {
  align: PropTypes.string,
  onClick: PropTypes.func.isRequired,
  type: PropTypes.string,
}

export default cssModules(Button, styles)

And here's the stylesheet so far:

@import "~components/styles/variables"

.button
  color: $button-default
  background-color: transparent
  font-family: $font-family
  font-size: $default-font-size
  font-weight: $font-regular
  line-height: $default-button-height
  margin: 0 $pad 0 0
  outline: none
  padding: 0 $pad*2

.left
  float: left

.right
  float: right

.primary
  color: $background-interaction
  background-color: $button-default

.button-left
  composes: button, left

.button-right
  composes: button, right

.button-primary
  composes: button, primary

.button-primary-left
  composes: button, primary, left

.button-primary-right
  composes: button, primary, right

Right now, it's pretty painful. Every configurable prop we add exponentially increases the number of composed classes we have to provide. We can currently configure align and type, and since both can be null we have 6 possible combinations, so 5 composed classes to create in addition to the base .button. If we added just one more prop, say just a boolean bold, we now have to add a whole bunch of new composed class names: .button-bold, .button-left-bold, .button-right-bold, .button-primary-bold, .button-primary-left-bold, .button-primary-right-bold.

I know with react-css-modules we can just enable the allowMultiple setting to allow us to specify multiple modules to apply to an element, but my understanding is that is against best practices. I feel like we have to be missing something here. What are we doing wrong?

Ad

Answer

I think maybe there's a mix of concerns in the example, and that's why it doesn't fit the "one class per element" rule. One of the reasons to enforce the one class per element rule is so that we can easily change the appearance of an element's state without actually touching the element. (Basically the promise of CSS itself, finally realized.) If you have multiple classes on an element, it's difficult to control the appearance without changing the element. This is especially true if you use appearance (rather than semantic) classes.

A class like "button-primary-left-bold" has some semantic meaning ("button-primary"), but it also has some layout meaning ("left") and some text appearance meaning ("bold"). A Button component probably has no business controlling its own layout. Remember, you can compose React components too! So you can have something more like:

<Left><Button type="primary">Click Me!</Button></Left>

Now the CSS module for the Button component can worry just about the types of buttons. And the Button component can be used anywhere, with any layout, not just a float-based layout.

Even better, the float could be pushed into a more semantic component as well. Perhaps something like:

<ButtonBar>
  <Button>Cancel</Button>
  <Button type="primary">Save</Button>
</ButtonBar>

The ButtonBar can have its own CSS module that does the layout. And later you can swap out that janky float layout for a swanky flexbox layout. :-)

Your "bold" modifier example certainly has no place inside the Button component. Better to think about why something is bold, and then make a component or semantic property for that. Otherwise, if the design calls for changing those "bold" buttons to "italic green" buttons, you have to go around changing a bunch of elements.

If you do this (exchange visual/layout classes for semantic components and break up components and CSS modules), you'll have less "exponential increases". If you do end up in a situation where you really do have a need for multiple semantic properties being combined, there is still some value in having those states be spelled out. A good example might be "primary-disabled". This is better than "primary disabled" for a couple of reasons. First, it's easy to look in the CSS module and see the explicit state listed. Second, there aren't ambiguous uses and relationships of those classes. Is "primary disabled" actually a valid use of those classes? That can only be known if somebody documents that use. The "disabled" class may override something in the "primary" class, meaning there are implicit ordering dependencies. It's easy for a well-meaning edit in the future to break things, because the relationship between those classes isn't obvious. As is often the case, choosing something implicit to save some keystrokes can lead to subtle errors. Paying that bit of keystroke tax locks things in and makes it obvious what will work and what won't.

Another small point that will save you a few keystrokes: there's really no reason for the "button-" prefix. That's exactly what CSS modules are for. Do this instead:

.normal
  color: $button-default
  background-color: transparent
  font-family: $font-family
  font-size: $default-font-size
  font-weight: $font-regular
  line-height: $default-button-height
  margin: 0 $pad 0 0
  outline: none
  padding: 0 $pad*2

.primary
  composes: normal
  color: $background-interaction
  background-color: $button-default

The filename itself essentially is the "button" prefix.

Ad
source: stackoverflow.com
Ad