Skip to content

Instantly share code, notes, and snippets.

@gaearon
Last active March 23, 2018 17:18
Show Gist options
  • Save gaearon/84f04ff35334ed80dcaf to your computer and use it in GitHub Desktop.
Save gaearon/84f04ff35334ed80dcaf to your computer and use it in GitHub Desktop.
BEM in React
'use strict';
var React = require('react'),
classSet = require('react/lib/cx'),
_ = require('underscore');
var ClassNameMixin = {
propTypes: {
className: React.PropTypes.string,
context: React.PropTypes.string
},
getClassName() {
var componentClassName = this.className || this.constructor.displayName,
classNames = [componentClassName],
context = this.props.context,
modifiers;
if (this.getCSSModifiers) {
modifiers = this.getCSSModifiers();
} else {
modifiers = [];
}
if (_.isObject(modifiers) && !_.isArray(modifiers)) {
modifiers = classSet(modifiers).split(' ');
}
if (context) {
modifiers.push('isIn' + context[0].toUpperCase() + context.slice(1));
}
if (this.props.className) {
classNames = classNames.concat(this.props.className.split(' '));
}
classNames = _.union(
classNames,
_.compact(modifiers).map(m => componentClassName + '--' + m)
);
return classNames.join(' ');
}
};
module.exports = ClassNameMixin;
@gaearon
Copy link
Author

gaearon commented Sep 11, 2014

This is the mixin we at Stampsy use for supporting something BEM-like.
We mix it into almost every component.

It allows you to specify className and getCSSModifiers on component to translate props into BEM modifiers.

What it gives you:

// <div class='Button Button--dark'>
<Button />
<Button color='dark' />

// <div class='Button Button--light'>
<Button color='light' />

// <div class='Button Button--light Button--active'>
<Button color='light' active />

// Special case: sometimes you want to force a class
// <div class='Button Button--active someSpecialClass'>
<Button active className='someSpecialClass' />

// Special case: sometimes you want to “work around” BEM and explicitly tell component what it is inside of
// <div class='Button Button--active Button--isInPopover'>
<Button active context='popover' />

Sample component:

var Button = React.createClass({
  mixins: [ClassNameMixin],

  propTypes: {
    color: PropTypes.oneOf(['dark', 'light']).isRequired,
    active: PropTypes.bool
  },

  className: 'Button',

  getDefaultProps() {
    return {
      color: 'dark'
    };
  },

  getCSSModifiers() {
    // Return an array or an object--whichever is more convenient.
    // Falsy values will be ignored.

    // Array is convenient when most prop values are modifiers (like `color`)
    return [
      this.props.color,
      this.props.active && 'active'
    ];

    // Object is convenient when most props are flags (like `active`)
    return {
      active: this.props.active,
      dark: this.props.color === 'dark',
      light: this.props.color === 'light'
    };
  },

  render() {
    return (
      <div className={this.getClassName()}>
        {this.props.children}
      </div>
    );
  }
});

@mjackson
Copy link

Hey @gaearon! I totally love this idea. Nice work :)

Here's another take on it, without the underscore dependency. This one also allows you to return a string from getCSSModifiers if you like:

var React = require('react');
var classSet = require('react/lib/cx');

function addClassNames(set, classNames, prefix) {
  prefix = prefix || '';

  if (typeof classNames === 'string')
    classNames = classNames.split(/\s+/);

  if (Array.isArray(classNames)) {
    for (var i = 0, len = classNames.length; i < len; ++i)
      if (classNames[i])
        set[prefix + classNames[i]] = true;
  } else if (classNames) {
    for (var name in classNames)
      set[prefix + name] = classNames[name];
  }
}

var ClassName = {

  propTypes: {
    className: React.PropTypes.string
  },

  getClassName: function () {
    var classNames = {};

    classNames[this.className] = true;

    if (this.getCSSModifiers)
      addClassNames(classNames, this.getCSSModifiers(), this.className + '--');

    if (this.props.className)
      addClassNames(classNames, this.props.className);

    return classSet(classNames);
  }

};

module.exports = ClassName;

Cheers!

@gaearon
Copy link
Author

gaearon commented Sep 20, 2014

@mjackson (are mentions broken here?), it sure looks cleaner that way!
Thank you 👍

@mjackson
Copy link

mjackson commented Oct 7, 2014

Haha, yeah I think mentions are broken :) yw!!

@ryanflorence
Copy link

this is convenient for the original author, but one of the reasons to use BEM is to make your code greppable, so that you can easily identify where selectors are used. If you're concatting them with JS, you lose this.

@Couto
Copy link

Couto commented Oct 13, 2014

@rpflorence Would you suggest some other approach, or do you just explicitly set all the classnames?

@Couto
Copy link

Couto commented Oct 13, 2014

Wow that file doesn't help legibility at all, but I think I can see your point. Thanks for the link :)

@gaearon
Copy link
Author

gaearon commented Nov 2, 2014

one of the reasons to use BEM is to make your code greppable, so that you can easily identify where selectors are used.

Well, maybe, but for me the win is mostly no cascading. I don't need it being greppable because of consistent naming. I know SomeComponent* styles always live in SomeComponent.less.

@gaearon
Copy link
Author

gaearon commented Nov 2, 2014

Also, class name is only used for root selectors (for components themselves). Children selectors (Something-somePart) stay greppable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment