substyle
substyle is a simple helper function for writing reusable React components that are stylable through both, CSS and inline styles. In its core, it is nothing more than a simple mapping of the style
and className
prop values:
({ style, className }, key) => ({
className: key ? className && `${className}__${key}` : className,
style: key ? style && style[key] : style
})
Motivation
First, let's clarify the term reusable component. Reuse here does NOT mean components being used in multiple other components inside a single React application, but refers to reuse across multiple applications. Thus, reusable components are usually distributed as npm modules. While you can of course include CSS files in npm modules, there is no way of actually coupling reusable components and their stylesheets without making assumptions about the environment. Many libraries make do with using some prefixed class names and pointing users to a CSS file in the readme, however, this has the following problems: It's all too easy to forget about including the CSS (or removing it once the component is not used anymore) and there's always a risk of global class name conflicts.
Eliminating CSS in favor of inline styles can solve these problems as inline style definitions are properly coupled with components and there is no need to deal with global class names. So when distributing React components, ship inline styles instead of stylesheets. However, you have to ensure that users of your components can fully customize their styling. substyle helps you do just that by designating one consistent scheme for both:
- selecting the right parts of the nested inline styles for each rendered child element
- deriving an optional and customizable CSS class name for each rendered child element
Installation
npm install --save substyle
Example
Let's create a reusable Popover
component using substyle:
import substyle from 'substyle'
const Popover = (props) => (
<div {...substyle(props)}>
<button {...substyle(props, 'close')}>x</button>
{ props.children }
</div>
)
That's it: Our Popover
component can now be used with CSS as well as with inline styles.
For using CSS, assign className
<Popover className='popover'>
...
</Popover> // ...
For using inline styles, assign style
<Popover style={{
background: 'white',
close: { right: 0 }
}}>
...
</Popover>
So what does this achieve? First, it assigns class names derived from the element's className
prop. If the component is used without a className
, it's safe to assume that none of the child elements require a class name to be set. Second, if a style
object is supplied to the element, it selects the nested sub object under the specified key for the child element. By using the same keys for derived class names and nested element style definitions, a consistent naming scheme is established.
How to use it
substyle picks up the idea of BEM methodology of breaking style definitions down into Block, Element, and Modifier parts. In fact, substyle generates derived class names that follow BEM syntax. You don't have to know anything about BEM other than understanding how the three basic terms relate to React components. And this is straightforward:
- block: component
- element: descendant element of a component
- modifier: represents a specific state or variation of a block or element
Block
A React component's render function always returns exactly one React element. This container element, often a div
, wraps all descendant elements and represents the component block in the DOM. As such, it is assigned the block name as class name and receives the direct style definitions of the nested inline style object. For deriving the block styles, simply call substyle(props)
without a second argument.
Example
const Foo = (props) => <div {...substyle(props)}>...</div>
<Foo className='foo' />
<Foo style={{ // <div style='position: absolute; top: 0;'>...</div>
position: 'absolute',
top: 0,
bar: {
width: '100%'
}
}}
/>
Elements
For all React elements returned by the component's render
function other than the root container, substyle(props, key)
is used with a key
string as second argument, which identifies the element's type or role within the component. As a result, the corresponding DOM node is assigned a BEM class name of the form 'block__element'
on the one hand, and, on the other hand, receives all nested inline style definitions found under the nested property named key
of the passed style prop.
Example
const Foo = (props) => <div {...substyle(props)}>
<div {...substyle(props, 'bar')} />
<div {...substyle(props, ['bar', 'baz'])} />
</div>
<Foo className='foo' />
<Foo style={{ // <div style='position: absolute; top: 0;'>
position: 'absolute', // <div style='width: 100%' />
top: 0, // <div style='width: 100%' />
// </div>
bar: {
width: '100%'
}
}}
/>
As you can see in this example, it's possible to use an array of element keys. In turn, the element receives multiple class names, one derived for each key item. At the same time, nested inlines styles are picked from all properties found for these keys.
Modifiers
Modifiers are used to set styles conditionally based on props or state. If used on the container element, a class name of the form 'block block--modifier'
will be set on that node. In addition to the direct inline style definitions, it will receive the style definitions found nested under the '&modifier'
property. When using CSS, the additional modifier class name on the block container is already sufficient for being able to customize all nested elements according to the modification, as we can simply write selectors such as .block--modifier .block__element
. However, for supporting customization through inline styles, we have to explicitly set the modifiers in the substyle
calls for all nested elements as well.
Example
const Foo = ({ disabled, ...rest }) => (
<div {...substyle(rest, { '&disabled': disabled })}>
<div {...substyle(rest, { bar: true, '&disabled': disabled })} />
</div>
)
<Foo className='foo' />
<Foo className='foo' disabled />
const style = {
position: 'absolute',
top: 0,
bar: { width: '100%' },
'&disabled': {
opacity: 0.5,
bar: { width: '50%' },
}
}
<Foo style={style} />
<Foo style={style} disabled />
Default styles
In many cases, a component defines some default inline styles. User custom styles should than be merged with the these styles. substyle includes a small convenience function for this purpose: The defaultStyle
factory function is called with the default styles object, and returns a substyle function which is preconfigured to merge the style
prop with the default style (using lodash's merge function).
import { defaultStyle } from 'substyle'
const substyle = defaultStyle({
position: 'relative',
foo: {
position: 'absolute'
}
})
css modules
TODO
implementation ideas:
- let users pass a
classNames
containing a nested object mapping element / modifier keys to generated css module class names - OR only derive a unique block class names via automatically and derive all element class names the same way as now (requires a custom generateScopedName function passed with the postcss options)
Real world examples
If you want to see how substyle is used in practice, go have a look at code of existing React components already using it: