Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS Module composition

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?

like image 810
Brandon Konkle Avatar asked Dec 17 '15 00:12

Brandon Konkle


People also ask

What is a CSS module?

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default. CSS Modules let you write styles in CSS files but consume them as JavaScript objects for additional processing and safety.

What is CSS modules in React?

CSS Modules are "CSS files in which all class names and animation names are scoped locally by default". Instead of having CSS files and classes that are static, CSS Modules creates a dynamic implementation that is locally scoped to the HTML with the help of Webpack or Browserify.

How do I import a CSS module?

To import a CSS Module into the corresponding component, import the css modules stylesheet as styles or [name]Styles : In JSX, use the defined CSS class as a className prop like so: From here, you can add as many other CSS modules you'd like, just remember to import each as a different name.


2 Answers

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.

like image 134
Justin Deal Avatar answered Oct 17 '22 19:10

Justin Deal


I think I'd move away from compose in this case and nest your classes. Here's my suggestion (pardon me if my jsx is a tad off):

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 the SASS:

@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

Fully acknowledging that "left" and "primary" could conflict with other class names in your app. So it might not be a bad idea to come up with some slightly better (more scoped) names.

like image 2
Nick Malozzi Avatar answered Oct 17 '22 18:10

Nick Malozzi