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?
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.
Related Questions
- → Import statement and Babel
- → should I choose reactjs+f7 or f7+vue.js?
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → .tsx webpack compile fails: Unexpected token <
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → React Native with visual studio 2015 IDE
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM
- → How do I determine if a new ReactJS session and/or Browser session has started?
- → Alt @decorators in React-Native
- → How to dynamically add class to parent div of focused input field?