I adore the styled pattern for composing React components, however, I also love css-modules and separating concerns. Life isn't all sunshine and roses though. Complex class compositions often result in ugly inline ternary operators for conditional class names and style modifiers. I wanted to create a compromise, or "best-of-both-worlds" solution, that wraps a standard css-modules implementation in a well-established API.
There are some trade-offs with a non-css-in-js solution though. Since it still outputs a build-time compiled stylesheet, runtime styles are a no-no*. While preprocessors — like SCSS — go a long way to bridge that gap, they don't completely alleviate the problem. But if you don't need on-the-fly styling, or you're going to use css-modules anyway, then hopefully this can be the solution for you too!
* Rejoice! This is no longer true, see the Dynamic Styles section on using and implementing runtime styling.
- Example
- Style Modifiers
- Sharing Styles
- Using
as
- Using
attrs
- Additional Styles
- Multiple Base Class Names
- TypeScript
- Dynamic Styles
- Browser Support
- Badge
- Built With
chic-modules
- Contributing
- License
- Acknowledgments
// application.module.css
.wrapper {
padding: 4em;
background: papayawhip;
}
.title {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
// application.jsx
import React from 'react';
import styles from './application.module.css';
import { create } from 'chic-modules';
// Call the chic-modules `create` factory and pass the
// required styles object as an argument
const styled = create(styles);
// Create a <Wrapper> React component that inherits the `.wrapper`
// class from the styles object and renders a <section> html element
const Wrapper = styled.section('wrapper');
// Create a <Title> React component that inherits the `.title`
// class from the styles object and renders a <h1> html element
const Title = styled.h1('title');
// Use them like regular React components – except they're styled!
function Application() {
return (
<Wrapper>
<Title>Hello World, this is my first chic component!</Title>
</Wrapper>
);
}
This is what you'll see in your browser:
As I briefly touched upon in the opening Motivation statement, my biggest gripe when using css-modules is the cumbersome nature of adding "modifier" class names to components. Where I believe chic-modules
really shines is in its attempt to solve this problem.
Taking a look at this pretty standard setup using the classnames package, you can see that a lot of extra steps are required to attach conditional class names to a component. This problem only gets worse when you try to go it alone without a class name utility package.
import classnames from 'classnames';
import styles from './button.module.css';
function MyButton({ children, isPrimary }) {
const classes = classnames(
'button',
{
[styles['button--primary']]: isPrimary
}
);
return <button className={classes}>{children}</button>;
}
// outputs <button class="button button--primary">
On the other hand, chic-modules
can infer when a prop is being used as a style modifier and automagically add the relevant modifier class if it exists in the styles object to the component.
import styles from './button.module.css';
import { create } from 'chic-modules';
const styled = create(styles);
const Button = styled.button('button');
function MyButton({ children, isPrimary }) {
return <Button isPrimary={isPrimary}>{children}</Button>;
}
// outputs <button class="button button--primary">
Any prop can be used to infer a style modifier as long as it starts with has
, is
or with
and its value evaluates as truthy. You can also pass string values to props prefixed with with
and have that value used in the constructed modifier class.
chic-modules
expects that your styles follow the BEM naming convention, so when using this package ensure that your stylesheet aligns with this structure.
<Button hasBorder isPrimary withTextColor="black" />
// outputs <button class="button button--border button--primary button--text-color-black">
You can extend an existing "chic" component, or just about any component so long as it accepts the className
prop, and supply it with the class names you wish to attach.
// button.module.css
.button {
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
}
.tomato-button {
color: tomato;
border-color: tomato;
}
// button.jsx
const Button = styled.button('button');
// outputs <button class="button">
const TomatoButton = styled(Button, 'tomato-button');
// outputs <button class="button tomato-button">
In addition, you can also override the underlying HTML element by passing the as
prop — which accepts either a string or another component. Using another component as the value will also extend its styles, similar to the above example.
const Button = styled.button('button');
const TomatoButton = styled.button('tomato-button');
// The component will render as a `div` element instead of a `button`
<Button as='div' />
// The component will inherit the properties of, and render as, the
// `TomatoButton` component
<Button as={TomatoButton} />
Sometimes you know ahead of time that your component is always going to have the same static props, such as an input element having a type
property. By using the attrs
constructor you can implicitly set any static prop values that should be passed down to every instance of your "chic" component.
const TextField = styled.input.attrs({ type: 'text' })('input-text');
// This will render with the `type` attribute implicitly set
// from the original declaration
<TextField />
// You can also locally override any attributes that are defined above
<TextField type='email' />
// For extended components, you can define attributes in the same way
const EmailField = styled.attrs({ type: 'email' })(TextField, 'input-email');
When extending a component, you may need to reference an additional style object from the one you used during the initial create
call. While you could use JavaScript to merge all the required objects together, chic-modules
allows you to pass an additional style object as a final argument. This way you can keep your code clean and module structure in-tact.
import buttonStyles from './button.module.css';
import tomatoButtonStyles from './tomato-button.module.css';
import { create } from 'chic-modules';
const styled = create(buttonStyles);
const Button = styled.button('button');
const TomatoButton = styled(Button, 'tomato-button', tomatoButtonStyles);
<Button />
// outputs <button class="button">
<TomatoButton />
// outputs <button class="button tomato-button">
In fact, if you prefer, you can completely omit passing a styles object to the create
call and instead supply your styles object directly to the component construction method as required.
import buttonStyles from './button.module.css';
import { create } from 'chic-modules';
const styled = create();
const Button = styled.button('button', buttonStyles);
<Button />
// outputs <button class="button">
When instantiating a "chic" component, as an alternative to passing a single class name argument, you can also supply an array of class names. This is useful when you need your component to inherit styles from multiple sources. In addition to this, any style modifying prop will apply to each base class name as long as the modifier exists in the style object.
However, if you need to apply a series of static class names, for use with a third-party library for instance, it is better to add them via the attrs
constructor or the className
prop instead.
const Heading = styled.h1(['heading', 'homepage-heading']);
<Heading />
// outputs <h1 class="heading homepage-heading">
<Heading hasUnderline />
// outputs <h1 class="heading heading--underline homepage-heading homepage-heading--underline">
chic-modules
comes built-in with type definitions, making it super easy to get started with your TypeScript project.
If you want to ensure your "chic" components are type-safe then pass your Type Assertions in the following way:
interface ButtonProps {
size: 'small' | 'large';
}
const Button = styled.button<ButtonProps>('button');
// Oops! This will throw a type error because the `size` prop
// has not been defined
<Button />
// Life in beautiful type-safe harmony
<Button size='small' />
While the primary focus of chic-modules
is to provide a better developer experience when working with css-modules and React, it also provides built-in functionality for handling instances where dynamic runtime styling is required.
In the below example, we have a React component that accepts some children and wraps them in above and below spacing. Utility components like this can be useful when creating layouts but depending on your requirements can result in needing to include dozens of additional modifiers.
By passing down your dynamic styles as a prop, chic-modules
will automatically create a unique class and insert it into your document. Any updates to the component's prop value will then generate a new class attach itself to the component.
chic-modules
inserts all dynamic styles in a style
element with the data-chic
attribute within the document head. You can either control the placement and add the element yourself, or chic-modules
will automatically create it for you.
import styles from './spacer.module.css';
import { create } from 'chic-modules';
const styled = create(styles);
const Spacer = styled.div('spacer');
function MySpacer({ bottom = 0, children, top = 0 }) {
return <Spacer style={{ paddingBottom: bottom, paddingTop: top }}>{children}</Spacer>;
}
// outputs <div class="spacer cmq487y5">
For SSR pages, using the extractDynamicStyles
function you can grab any component dynamic styles and include them in the response HTML. Similar to the above, extracted dynamic styles from chic-modules
must be placed within a <style>
element with the data-chic
attribute.
import { extractDynamicStyles } from 'chic-modules';
// In your HTTP response, return the base page HTML as well as your extracted dynamic styles.
// Note: This setup needs to be done in addition to the regular implementation of your css-module styles
return `
<html>
<head>
<style data-chic>${extractDynamicStyles()}</style>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`;
Open example using Next.js in CodeSandbox
chic-modules
should work in all major modern browsers out-of-the-box (Chrome, Edge, Firefox, Safari).
To add support for browsers IE 11 and older, ensure you add polyfills for the following features:
Sing loud and proud! Let the world know that you're using chic-modules
[![styled with: chic-modules](https://img.shields.io/badge/styled%20with-%E2%9C%A8%20chic--modules-blue?style=flat-square)](https://github.com/eels/chic-modules)
Thanks for taking the time to contribute! Before you get started, please take a moment to read through our contributing guide. The two focus areas for chic-modules
right now is increasing performance and fixing potential bugs.
However, all issues and PRs are welcome!
MIT - see the LICENSE.md file for details
- Originally inspired by parts of the styled-components API
- With additional optimisation inspiration from the 1KB alternative - goober