Using BEM in React ecosystem
naming inconsistency
hard to read
hard to maintain
hard to add functionality
styles are not isolated
unpredictable side-effects
easy to break something
hard to scale
The Easy
The Hard
The Ugly
BEM
CSS modules
CSS in JS
import Radium from 'radium';
import React from 'react';
import color from 'color';
const styles = {
base: {
color: '#fff',
':hover': {
background: color('#0074d9').lighten(0.2).hexString()
}
},
warning: {
background: '#FF4136'
}
};
@Radium
class Button extends React.Component {
render() {
return (
<button
style={[
styles.base,
styles[this.props.kind]
]}>
{this.props.children}
</button>
);
}
}
import Radium from 'radium';
import React from 'react';
import styles from './styles';
@Radium
class Button extends React.Component {
render() {
return (
<button
style={[
styles.base,
styles[this.props.kind]
]}>
{this.props.children}
</button>
);
}
}
import Radium from 'radium';
import React from 'react';
@Radium
class Button extends React.Component {
render() {
const styles = { this.props };
return (
<button
style={[
styles.base,
styles[this.props.kind]
]}>
{this.props.children}
</button>
);
}
}
import useSheet from 'react-jss'
import React from 'react';
import jss from 'jss';
import vendorPrefixer from 'jss-vendor-prefixer';
jss.use(vendorPrefixer);
const styles = {
button: {
'background-color': 'yellow'
},
label: {
'font-weight': 'bold'
}
};
@useSheet(styles)
class Button extends React.Component {
render() {
const { classes } = this.props.sheet;
return (
<div className={classes.button}>
<span className={classes.label}>
{this.props.children}
</span>
</div>
);
}
}
REPL: jsstyles.github.io/repl
js is the most powerful “preprocessor”
absolute isolation
using style attribute is crazy and unperformant*
inline stylesheets are not cacheable*
not flexible like traditional css (no cascade, etc.)
no autocomplete, Emmet, post-css, autoprefixer, etc.
mixing concerns
styling becomes... hard
/* styles.css */
.table {
width: 100%;
}
.row {
font-size: 10px;
}
.cell {
background: url('cell.png');
}
/* index.js */
import React from 'react';
import styles from './styles.css';
class extends React.Component {
render () {
return (
<div className={styles.table}>
<div className={styles.row}>
<div className={styles.cell}>A0</div>
<div className={styles.cell}>B0</div>
</div>
</div>
);
}
}
/* styles.css */
.Table__table___sVK0p {
width: 100%;
}
.Table__row___Tz7_C {
font-size: 10px;
}
.Table__cell___3b3O_ {
background: url('cell.png');
}
<!-- index.html -->
...
<div class="Table__table___sVK0p">
<div class="Table__row___Tz7_C">
<div class="Table__cell___3b3O_">A0</div>
<div class="Table__cell___3b3O_">B0</div>
</div>
</div>
...
absolute isolation
it's good old css
cacheable
not flexible like traditional css (no cascade, etc.)
debugging is hard
theming is hard (my modest attempt to solve it)
BEM is a methodology, that makes your front-end code reusable, scalable,
more strict and explicit
BEM compliments React: Block ≈ Component
it's good old css
intuitive and familiar abstraction — easy to read/debug
theming is much easier (more on this later)
isolation through strict code convention
isolation is not bullet-proof — requires some discipline
cacheable
cascade is possible
import React from 'react';
class Popup extends React.Component {
render() {
return (
<div className={'popup' + (this.props.visible ? ' popup_visible' : '')}>
<div className="popup__overlay"></div>
<div className="popup__content">{this.props.children}</div>
</div>
);
}
}
import React from 'react';
import b from 'b_';
const block = b.with('popup');
class Popup extends React.Component {
render() {
return (
<div className={b({ visible: this.props.visible })}>
<div className={b('overlay')}></div>
<div className={b('content')}>{this.props.children}</div>
</div>
);
}
}
import React from 'react';
import BEMHelper from 'react-bem-helper';
const classes = new BEMHelper({ name: 'popup' });
class Popup extends React.Component {
render() {
let block = classes();
if (this.props.visible) {
block = classes({ modifiers: 'visible' });
}
return (
<div {...block}>
<div {...classes('overlay')}></div>
<div {...classes('content')}>{this.props.children}</div>
</div>
);
}
}
import BemReact from 'bem-react';
const Popup = BemReact.createClass({
render() {
return {
block: 'popup',
mods: {
visible: this.props.visible
},
content: [
{
block: 'popup',
elem: 'overlay'
},
{
block: 'popup',
elem: 'content',
content: this.props.children
}
]
};
}
});
import Yummies from '@yummies/yummies';
class Popup extends Yummies.Component {
render() {
return {
block: 'popup',
mods: {
visible: this.props.visible
},
content: [
{
elem: 'overlay'
},
{
elem: 'content',
content: this.props.children
}
]
};
}
}
import Yummies from '@yummies/yummies';
import Input from '../input';
class Checkbox extends Input {
// ...
render() {
const template = super.render();
template.block = 'checkbox';
template.mods = {
...template.mods,
checked: this.state.checked
};
return template;
}
}
import React from 'react';
import { BEM as B } from 'rebem';
class Popup extends React.Component {
render() {
return B(
{
block: 'popup',
mods: { visible: this.props.visible }
},
B({ block: 'popup', elem: 'overlay' }),
B({ block: 'popup', elem: 'content' }, this.props.children)
);
}
}
import React from 'react';
import { blockFactory } from 'rebem';
const B = blockFactory('popup');
class Popup extends React.Component {
render() {
return B(
{
mods: { visible: this.props.visible }
},
B({ elem: 'overlay' }),
B({ elem: 'content' }, this.props.children)
);
}
}
import React from 'react';
class Popup extends React.Component {
render() {
return (
<div block="popup" mods={{visible: this.props.visible}}>
<div block="popup" elem="overlay" />
<div block="popup" elem="content">{this.props.children}</div>
</div>
);
}
}
import Button from '../button';
class DeleteButton extends Button {
onClick = e => {
// some custom stuff
};
render() {
const template = super.render();
template.mods = { type: 'delete' };
template.content[0].props = {
...template.content[0].props,
onClick: this.onClick
};
return template;
}
}
import React from 'react';
import Button from '../button';
class DeleteButton extends React.Component {
onClick = e => {
// some custom stuff
};
render() {
return (
<Button {...this.props} mods={{ type: 'delete' }} onClick={this.onClick}>
{this.props.children}
</Button>
);
}
}
<div className='popup popup_visible'></div>
visible popup
div with classnames “popup” and “popup_visible”
“popup” block with modifier “visible”
visible popup
div with classnames “popup” and “popup_visible”
“popup” block with modifier “visible”
<div block="popup" mods={{visible: true}}></div>
time + effort
human level
machine level
reBEM
classNames
visible popup
block with modifier
div with className
time + effort
human level
machine level
One iteration with reBEM
One iteration with classNames
import React from 'react';
import Modal from 'react-modal2';
import { stringify as b } from 'rebem-classname';
const block = 'popup';
class Popup extends React.Component {
render() {
return (
<Modal
backdropClassName={b({ block, elem: 'overlay' })}
modalClassName={b({ block, mods: { visible: this.props.visible } })}>
{this.props.children}
</Modal>
);
}
}
+
const component = shallow(
<Popup visible={true} />
);
it('should be popup block', function() {
expect(component).to.be.a.block('popup');
});
it('should have visible modifier', function() {
expect(component).to.have.mods({ visible: true });
});
it('should have overlay element', function() {
expect(component.findBEM({ block: 'popup', elem: 'overlay' })).to.have.length(1);
});
it('should have content element', function() {
expect(component.findBEM({ block: 'popup', elem: 'content' })).to.have.length(1);
});
+
// example is in less, but you can use any preprocessor or just vanilla CSS
:block(popup) {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
&:mod(visible) {
opacity: 1;
}
&:elem(overlay) {
background: rgba(0, 0, 0, 0.5);
}
&:elem(content) {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
// common.blocks
modules.define('button',
// ...
onSetMod: {
'disabled': {
'true': function() {
this.elem('control').attr('disabled', true);
this.delMod('focused');
}
}
}
// ...
);
// desktop.blocks
modules.define('button',
// ...
onSetMod: {
'disabled': {
'true': function() {
this.__base.apply(this, arguments); // basically super() call
this.delMod('hovered'); // hover-related stuff
}
}
}
// ...
);
Shareable and composable sets of components allowing to:
core components
reset theme
product theme
app layer
most frequently used
(Button, Input, Select, Link, Popup, Tabs, ...)
reducing browser inconsistencies
(normalize.css, Reset CSS, ...)
company/product theme
(company colors, logos, icons, fonts, ...)
app specific stuff
(components, sub-theme, modifiers, ...)
custom components
shared between your apps
(Calendar, Notifications,
core components modifications, ...)
less specific
more specific
common components
touch interfaces
mobile phones
tablets
desktop browsers
less specific
more specific
/*
.
└──/core-components
└── /button
└── /index.js
*/
export default function Button({ mods, mix, children, ...props }) {
return (
<label block="button" mods={mods} mix={mix}>
<input block="button" elem="control" type="button" {...props} />
{children}
</label>
);
}
/*
.
└──/theme-reset
└── /button
└── /styles.less
*/
.button {
display: inline-block;
box-sizing: border-box;
&__control {
box-sizing: border-box;
background: none;
appearance: none;
outline: 0;
border: 0;
padding: 0;
color: inherit;
font: inherit;
text-transform: none;
line-height: normal;
&::-moz-focus-inner {
border: 0;
padding: 0;
}
}
}
/*
.
└──/custom-components
└── /button
└── /index.js
*/
import Button from '#button';
export default class extends React.Component {
renderIcon(icon) {
if (icon) {
return <span block="button" elem="icon" style={{ backgroundImage: icon }} />;
}
return null;
},
render() {
return (
<Button {...props}>
{children}
{this.renderIcon(this.props.icon)}
</Button>
);
}
}
import Button from 'core-components/button/index.js';
import from 'theme-reset/styles.less';
// js from the last layer, styles from all previous layers
/*
.
└──/product-theme
└── /button
└── /index.js
└── /styles.less
*/
import Button from '#button';
export default function Button({ children, ...props }) {
return (
<Button {...props}>
{children}
<div block="button" elem="mask" />
</Button>
);
}
import Button from 'custom-components/button/index.js';
import from 'theme-reset/styles.less'; import from './styles.less';
// js from the last layer, styles from all previous layers
.button {
// ...
&__mask {
position: absolute;
background: #f00;
border: 2px solid #000;
}
}
/*
.
└──/app
└── /somewhere.js
*/
import Button from '#button';
class SomeAppComponent extends React.Component {
// ...
return (
//...
<Button
mix={{ block: 'my-app', elem: 'button' }}
icon="never-gonna-give-you-up.png"
onClick={doStuff}>
{'Click me'}
</Button>
//...
);
}
import Button from 'product-theme/button/index.js';
import from 'theme-reset/styles.less'; import from 'product-theme/styles.less';
// js from the last layer, styles from all previous layers
// ...
preLoaders: [
{
test: /\.js$/,
loader: 'rebem-layers',
query: {
layers: [
// shared layers
require('core-components'),
require('theme-reset'),
require('custom-components'),
require('product-theme'),
// app components
{
path: path.resolve('src/components/'),
files: {
main: 'index.js',
styles: 'styles.less'
}
}
],
// list of places where you will require components from layers
consumers: [
path.resolve('src/')
]
}
}
],
// ...
...but better together
reBEM packages are not coupled and can be used independently:
BEM methodology:
Official website + Community website
Github: https://github.com/mistadikay
Twitter: https://twitter.com/mistadikay
E-mail: iam@mistadikay.com