$ yarn add treat@next
Write your styles in JavaScript/TypeScript within treat files (e.g. Button.treat.js
) that get executed at build time.
All CSS rules are created ahead of time, so the runtime is very lightweight—only needing to swap out pre-existing classes. In fact, if your application doesn't use theming, you don't even need the runtime at all.
All CSS logic, including its dependencies, will not be included in your final bundle.
Because theming is achieved by generating multiple classes, legacy browsers are supported.
Requirements
Your project must be using webpack with the supplied webpack plugin, but that's it.
First-class support is provided for React and TypeScript, but those layers are entirely optional. The core runtime API can also be integrated into other frameworks, if needed.
The core runtime makes use of Map
, so you may need to provide a polyfill for pre-ES2015 browsers.
Example Usage
Basic Usage
First, define and export styles from a treat file.
// Button.treat.js
// ** THIS CODE WON'T END UP IN YOUR BUNDLE! **
import { style } from 'treat';
export const button = style({
backgroundColor: 'blue',
height: 48
}));
Then, import the styles.
// Button.js
import * as styles from './Button.treat.js';
export default ({ text }) =>
`<button class="${styles.button}">${text}</button>`;
Themed Usage
Note: React is not required to use treat.
First, create and export a theme from a treat file. Normally, you'd define multiple themes, but let's keep it short.
// theme.treat.js
// ** THIS CODE WON'T END UP IN YOUR BUNDLE! **
import { createTheme } from 'treat';
export default createTheme({
brandColor: 'blue',
grid: 4
});
Then, import the desired theme and pass it to TreatProvider
at the root of your application.
// App.js
import React from 'react';
import { TreatProvider } from 'react-treat';
import theme from './theme.treat.js';
export function App() {
return <TreatProvider theme={theme}>...</TreatProvider>;
}
Now that you've configured the theming system, define and export themed styles from a treat file.
// Button.treat.js
// ** THIS CODE WON'T END UP IN YOUR BUNDLE EITHER! **
import { style } from 'treat';
export const button = style(theme => ({
backgroundColor: theme.brandColor,
height: theme.grid * 11
}));
Then import and resolve themed styles via the useStyles
Hook.
// Button.js
import React from 'react';
import { useStyles } from 'react-treat';
import * as styleRefs from './Button.treat.js';
export function Button(props) {
const styles = useStyles(styleRefs);
return <button {...props} className={styles.button} />;
}
Setup
Webpack Setup
To get started, add the treat webpack plugin to webpack.config.js
. Since webpack is required to use treat, the webpack plugin is provided via the core treat
package as treat/webpack-plugin
.
const TreatPlugin = require('treat/webpack-plugin');
module.exports = {
plugins: [new TreatPlugin()]
};
By default, this will inject styles into the page via style-loader, but this can be overridden via the outputLoaders
option.
For example, if you'd like to export static CSS files, you can wire it up to mini-css-extract-plugin.
const TreatPlugin = require('treat/webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new TreatPlugin({
outputLoaders: [MiniCssExtractPlugin.loader]
}),
new MiniCssExtractPlugin()
]
};
For more configuration options, view the full webpack plugin API.
Babel Setup
In order to improve the debugging experience, treat also provides an optional Babel plugin.
First, install the plugin:
$ yarn add --dev babel-plugin-treat@next
Then, add it to your Babel config. For example, in .babelrc
:
{
"plugins": ["babel-plugin-treat"]
}
API Reference
Data Types
While not an exhaustive list of all types defined in the library, this section covers the core data types that are essential to using the library.
Styles
Type: object
When passing styles to the style
and styleMap
functions, or returning styles from a ThemedStyles
function, you'll need to define them in the following format.
{
color: 'red',
fontFamily: 'comic sans ms',
fontSize: 24
}
Simple psuedo selectors are supported at the top level.
{
color: 'red',
':hover': {
backgroundColor: 'pink'
},
':active': {
backgroundColor: 'tomato'
}
}
Media queries are also supported via the @media
key.
{
fontSize: 24,
'@media': {
'screen and (min-width: 768px)': {
fontSize: 42
}
}
}
For anything more advanced, you can provide a set of custom selectors. Within each selector, you must target the ampersand character (&
), which refers to the generated class name.
{
marginRight: 10,
selectors: {
'&:nth-child(2n)': {
marginRight: 0
}
}
}
Within selectors, existing treat classes can be referenced.
{
backgroundColor: 'white',
selectors: {
[`${parentClass} &`]: {
backgroundColor: 'aqua'
}
}
}
The @keyframes
property allows the creation of keyframes that will automatically be attached to the style as your animation-name
.
{
backgroundColor: 'white',
'@keyframes': {
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(359deg)',
},
},
animationTimingFunction: 'linear',
animationDuration: '1.5s',
}
The animation shorthand is also supported via a @keyframes
placeholder.
{
backgroundColor: 'white',
'@keyframes': {
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(359deg)',
},
},
animation: '@keyframes 1.5s linear'
}
ThemedStyles
Type: function
Accepts a Theme
and returns a Styles
object.
theme => ({
color: theme.brandColor
});
Theme
When defining themes and consuming themes, the provided theme object uses the Theme
type, which is any
by default. This means that any usage of a theme will not be type-safe.
The simplest way to fix this is to override this type at a global level. For example, you could create a treat.d.ts
file in your project with the following contents.
declare module 'treat/theme' {
export interface Theme {
brandColor: string;
grid: number;
}
}
If your Theme
type is already defined elsewhere in your application, you'll need to import it with a dynamic import
expression within the module declaration block.
declare module 'treat/theme' {
export type Theme = import('./types').Theme;
}
Alternatively, if you'd prefer to avoid global types, you can manually annotate the theme object being passed into a ThemedStyles
function.
import { style } from 'treat';
import { Theme } from './types';
const themedClass = style((theme: Theme) => ({
color: theme.brandColor
}));
Styling API
The following styling APIs are only valid within treat files (e.g. Button.treat.js
).
createTheme
Type: function
The createTheme
function allows you to register individual themes within a treat file.
import { createTheme } from 'treat';
const theme = createTheme({
brandColor: 'blue',
grid: 4
});
style
Type: function
The style
function allows you to create individual style rules within a treat file.
import { style } from 'treat';
export const brandColor = style(theme => ({
color: theme.brandColor;
}));
If your styles aren't dependent on the theme, you can provide a static object instead.
import { style } from 'treat';
export const green = style({
color: 'green'
});
styleMap
Type: function
The styleMap
function allows you to easily create multiple namespaces within a treat file.
import { styleMap } from 'treat';
export const variants = styleMap(theme => ({
primary: {
backgroundColor: theme.colors.brand
},
secondary: {
backgroundColor: theme.colors.accent
}
}));
This is particularly useful when mapping component props to separate style maps. For example, if you wanted to map these styles to a React component in TypeScript:
import React from 'react';
import { useStyles } from 'react-treat';
import * as styleRefs from './Button.treat';
export function Button({ variant = 'primary', ...props }) {
const styles = useStyles(styleRefs);
return (
<button
{...props}
className={styles.variants[variant]}
/>
);
}
This pattern scales extremely well to atomic CSS patterns. For example:
// atoms.treat.js
import { styleMap } from 'treat';
import { mapValues } from 'lodash';
const spacingTokens = {
small: 4,
medium: 8,
large: 16
};
const spacingStyles = property =>
mapValues(spacingTokens, value => ({
[property]: value
}));
export const padding = {
top: styleMap(spacingStyles('paddingTop')),
bottom: styleMap(spacingStyles('paddingBottom')),
left: styleMap(spacingStyles('paddingLeft')),
right: styleMap(spacingStyles('paddingRight'))
};
export const margin = {
top: styleMap(spacingStyles('marginTop')),
bottom: styleMap(spacingStyles('marginBottom')),
left: styleMap(spacingStyles('marginLeft')),
right: styleMap(spacingStyles('marginRight'))
};
// etc...
globalStyle
Type: function
The globalStyle
function allows you to define selector-based styles. This function is purely a side effect and does not create a new class.
import { globalStyle } from 'treat';
globalStyle('html, body', {
margin: 0,
padding: 0
});
Debugging
Note: This can be automated via our Babel plugin.
All styling APIs (except for globalStyle
) have an optional argument that allows you to provide a local debug name.
For example, the local name for the following style will be style
by default because treat doesn't have access to your variable name at runtime.
export const green = style({ color: 'green' });
To fix this, you can pass in a debug name as the last argument:
export const green = style({ color: 'green' }, 'green');
Runtime API
Note: If you're using React, you should use our React API instead.
resolveStyles
Type: function
Resolves styles for an entire treat file relative to a given theme.
import { resolveStyles } from 'treat';
import * as styleRefs from './styles.treat.js';
import theme from './theme.treat.js';
const styles = resolveStyles(theme, styleRefs);
resolveClassName
Type: function
Resolves a single treat class name relative to a given theme.
import { resolveClassName } from 'treat';
import theme from './theme.treat.js';
import * as styleRefs from './Button.treat.js';
const className = resolveClassName(theme, styleRefs.button);
React API
Note: React is not required to use treat.
TreatProvider
Type: Component
In order for the useStyles
and useClassName
Hooks to work, you'll need to render a TreatProvider
higher in the tree.
import React from 'react';
import { TreatProvider } from 'react-treat';
import theme from './theme.treat.js';
export function App() {
return <TreatProvider theme={theme}>...</TreatProvider>;
}
useStyles
Type: function
A React Hook that resolves styles for an entire treat file relative to the current theme.
import React from 'react';
import { useStyles } from 'react-treat';
import * as styleRefs from './Button.treat.js';
export function Button({ primary, ...props }) {
const styles = useStyles(styleRefs);
return <button {...props} className={styles.button} />;
}
useClassName
Type: function
A React Hook that resolves a single treat class relative to the current theme.
import React from 'react';
import { useClassName } from 'react-treat';
import * as styleRefs from './Button.treat.js';
export const Button = props => (
<button
{...props}
className={useClassName(styles.button)}
/>
);
Webpack Plugin API
option | description | default value |
---|---|---|
test |
Webpack condition targetting treat files. |
/\.treat.(js|ts)$/ |
outputCSS | Whether to output CSS into the bundle. Useful for dual config SSR apps. | true |
outputLoaders | Array of webpack loaders to handle CSS files, they will be placed after css-loader . Strings and objects with options are supported. |
['style-loader'] |
localIdentName | Template string for naming css classes. Should always contain a hash option to avoid clashes. |
Development: [name]-[local]_[hash:base64:5] Production: [hash:base64:5]
|
themeIdentName | Same as localIdentName but for themes. Useful for debugging which classes belong to which theme. Can also be a function that receives your theme. |
Development: _[name]-[local]_[hash:base64:4] Production: [hash:base64:4]
|
minify | Minify the output css | Inferred from webpack mode. Defaults to true if production mode. |
browsers | A browserslist query to pass to autoprefixer | By default, your browserslist query will be resolved from your browserslist config |
Thanks
-
Johannes Ewald for letting us have the
treat
name on npm. - Nathan Nam Tran for creating css-in-js-loader, which served as the initial starting point for our approach.
License
MIT.