Skip to content
Give your React components some style with Trousers πŸ‘–
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.storybook lint: make prettier happy, agian May 26, 2019
assets
src lint: make prettier happy, agian May 26, 2019
.eslintrc.js refactor: pass dependencies to useEffect Mar 19, 2019
.gitignore tools: update stroybook deployer script Mar 19, 2019
.prettierrc.js refactor: filter style definitions on the collector Mar 12, 2019
README.md updates readme Jun 6, 2019
jest.config.js build: ignore lib dir in jest Apr 12, 2019
package-lock.json 1.2.0 May 26, 2019
package.json 1.2.0 May 26, 2019
tsconfig.json refactor: mount classnames on the style tag data attribute Apr 12, 2019

README.md

Trousers, a little library for CSS-in-JS, without the mess

Trousers πŸ‘–

min npm Downloads per month

Give React Components some style with Trousers!

Try it here

Think of Trousers like styled-components + classnames + BEM, wrapped in a lovely React Hooks API ❀️. Trousers is designed to help you co-locate CSS and JS but opinionated in that it helps you avoid using JavaScript where CSS can take over. It loosely follows a BEM-like methodology, borrowing the concept of Blocks (the component), Elements (the child node you want to apply styles to) and Modifiers (apply styles when your component has particular props or state) to reduce the complexity that normally comes with CSS-in-JS.

Get started πŸ—

Installation

npm install --save trousers or yarn add trousers

Basic Example

Creating a trousered component

app/components/button.jsx

import { styleCollector, useStyles } from 'trousers';

const styles = styleCollector('button').element`
        background-color: ${theme => theme.backgroundColor};
        border: none;
        color: ${theme => theme.textColor};
        margin: 0 10px;
        padding: 10px 20px 14px 20px;

        :hover {
            background-color: ${theme => theme.hoverColor};
            color: rgba(255, 255, 255, 0.8);
        }
    `.modifier(props => !!props.primary)`
        background-color: #f95b5b;
        color: #ffffff;

        :hover {
            background-color: #e45454;
        }
    `;

const spanStyles = styleCollector('button-span').element`
        font-size: 20px;
        font-style: italic;
    `;

const Button = props => {
    const buttonClassNames = useStyles(styles, props);
    const spanClassNames = useStyles(spanStyles, props);

    return (
        <button className={buttonClassNames}>
            <span className={spanClassNames}>{props.children}</span>
        </button>
    );
};

export default Button;

app/MyApp.jsx

import { ThemeProvider } from 'trousers';

import Button from './components/button';

const theme = {
    backgroundColor: 'blue',
    textColor: 'white',
    hoverColor: 'lightblue',
};

const MyApp = props => {
    return (
        <ThemeProvider theme={theme}>
            <Button primary>How do I look?</Button>
        </ThemeProvider>
    );
};

export default Button;

Motivation

Unlike some of the more popular (and great!) CSS-in-JS libraries, Trousers has made the concious decision to avoid letting you directly apply Props to your CSS properties like this:

const Button = styled.button`
    /* Adapt the colors based on primary prop */
    background: ${props => (props.primary ? 'palevioletred' : 'white')};
    color: ${props => (props.primary ? 'white' : 'palevioletred')};

    font-size: 1em;
    margin: 1em;
    padding: 0.25em 1em;
    border: 2px solid palevioletred;
    border-radius: 3px;
`;

It's quite hard to see at a glance which state triggers which styles. The logic for a particular state can also tend to be duplicated across mutlitple properties. This is a simple example, consider the same example with multiple states like disabled, loading etc.

Trousers encourages semantics and allows you to group logic for different states into predicates, which are contained and easy to reason about at a glance. It leverages the C (cascade) in CSS to determine which styles are applied to an element when one or many states are active.

const buttonStyles =
    // Base styles, these are static and every modifier will be applied on top
    styleCollector('button').element`
        background-color: blue;
    `
        // A modifier for primary buttons. Note that the `cascade` will handle the color
        .modifier(props => props.primary)`
        background-color: red;
    `
        // Second modifier that will override the prior background-color rules
        .modifier(props => props.disabled)`
        background-color: grey;
    `;

Notice that you can localise the logic for a particular state in one place, which makes it more obvious to see which conditions will need to be met before a particular style set is applied.

Under the hood, Trousers will generate a hash, mount styles to the <head> of the page and return a human-readable class name. Then on, we are simply dealing with class names.

Enter Hooks

Hooks are a hot new feature in React, which allows Trousers to access context and state while abstracting the messy details away from the consumer. Our useStyles hook accepts a name, some props and an instance of styleCollector(). It will then evaluate everything for you and return a human-readable class name, which you can then apply to your desired element. For example, here we define a style for the button and inner span and apply the resulting classes to their respective elements.

const Button = props => {
    const buttonClassNames = useStyles(buttonStyles, props);
    const spanClassNames = useStyles(spanStyles, props);

    return (
        <button className={buttonClassNames}>
            <span className={spanClassNames}>{props.children}</span>
        </button>
    );
};

Theme Support

Theming is achieved via React's Context API, which provides a lot of flexibility. You can even choose to nest themes and present a section of your app in a different way. It looks a little something like this:

import { ThemeProvider } from 'trousers';

const lightTheme = {
    primaryColor: 'white',
    secondaryColor: 'blue',
    disabledColor: 'grey',
};

const darkTheme = {
    primaryColor: 'black',
    secondaryColor: 'purple',
    disabledColor: 'grey',
};

const MyApp = () => {
    return (
        <ThemeProvider theme={lightTheme}>
            <h1>Hello World</h1>
            <p>Rest of my app lives here and has access to the light theme!</p>
            <ThemeProvider theme={darkTheme}>
                <p>This subtree will have access to the dark theme!</p>
            </ThemeProvider>
        </ThemeProvider>
    );
};

When a Trousers component is mounted within a new theme context, it will render new styles and apply them to the component.

You can define how your component handles themes like this:

const buttonStyles = styleCollector('button').element`
        background-color: ${theme => theme.secondaryColor};
    `.modifier(props => props.primary)`
        background-color: ${theme => theme.primaryColor};
    `.modifier(props => props.disabled)`
        background-color: ${theme => theme.disabledColor};
    `;

Now your component will render different styles based on the context it is mounted in.

Global styles

Every app needs some form of global styling in order to import fonts or reset native styling, for example using @font-face would be quite challenging to use without access to globals.

Turns out that there's a hook for that, useGlobal:

import React, { useEffect } from 'react';
import { css, useGlobal } from 'trousers';

const globalStyles = css`
  @font-face {
    font-family: MyFont;
    src: url('${MyFont}') format('opentype');
  }
`;

const App = () => {
    const [clearStyles] = useGlobal(globalStyles);

    // clearStyles allows you to remove all global styles mounted by that component
    useEffect(() => () => clearStyles(), []);

    return <h1>Welcome to my website!</h1>;
};

Server side rendering (SSR)

Server side rendering with Trousers follows a similar approach to styled-components. It works by firstly instantiating a serverStyleRegistry, wrapping your application in a ServerProvider, then passing that registry into the provider as a prop. Then when you render your application to a string with react-dom/server, Trousers will push styles into the style registry. You can then pull the styles from the registry and manually append them to the head of your document.

import React, { FC, ReactNode } from 'react';
import { renderToString } from 'react-dom/server';

import { ServerStyleRegistry, ServerProvider } from 'trousers';
import App from './';

const registry = new ServerStyleRegistry();

const html = renderToString(
    <ServerProvider registry={registry}>
        <App />
    </ServerProvider>,
);

// Your styles will be accessible here
const styleTags = registry.get();

API

styleCollector() alias trousers()

The styleCollector() function is designed to collect style definitions and provide some portability. If you deside to define CSS in another file, you can do and re-import it into your component.

NOTE! styleCollector return methods will always return this, which means the calls can be chained repeatedly.

Arugments:

  • componentName: String

Returns:

  • styleCollector().element
  • styleCollector().modifier(predicate)
  • styleCollector().get()

styleCollector().element

A function which accepts a Tagged Template.

You should treat element blocks like you would with Elements in BEM.

  • The element name describes its purpose ("What is this?" β€” item, text, etc.), not its state ("What type, or what does it look like?" β€” red, big, etc.).
  • The structure of an element's full name is block-nameelement-name. The element name is separated from the block name with a double underscore ().
  • The block name defines the namespace, which guarantees that the elements are dependent on the block (block__elem)
  • A block can have a nested structure of elements in the DOM tree

Arugments:

  • taggedTemplate: TaggedTemplate

Example:

import { styleCollector } from 'trousers';

const styles = styleCollector('button').element`
        background-color: red;
    `;

styleCollector().modifier(predicate)

A function that accepts a predicate function or boolean and returns a new function which accepts a tagged template. The tagged template will only be rendered if the predicate returns a truthy value.

Note: Modifiers are dependant on order. Be sure to organise the order of your modifiers with the understanding that the bottom most modifer will potentially be overriding the style rules defined in the modifiers and elements delcared before it.

Modifiers follow the same methodoligy as Modifiers in BEM.

  • Defines the appearance, state, or behavior of a block or element
  • A modifier can't be used alone, a modifier can't be used in isolation from the modified block or element. A modifier should change the appearance, behavior, or state of the entity, not replace it
  • You can have one or multiple modifiers active at any time
  • The modifier name describes its appearance ("What size?" or "Which theme?" and so on β€” size_s or theme_islands), its state ("How is it different from the others?" β€” disabled, focused, etc.) and its behavior ("How does it behave?" or "How does it respond to the user?" β€” such as directions_left-top)

Arguments:

  • predicate: boolean | Function(props, state) => boolean

Returns:

  • Function(TaggedTemplate)

Example:

import { styleCollector } from 'trousers';

const styles = styleCollector('button').element``.modifier(props => {
    return props.primary;
})`
        background-color: yellow;
    `.modifier((props, state) => {
    return state.isActive;
})`
        background-color: purple;
    `.modifier(props => {
    return props.isDisabled;
})`
        background-color: grey;
    `;

styleCollector().get()

Outputs the collected styleDefinitions. StyleDefintions is an array of objects that trousers passes around internally.

StyleDefinition:

{
    styles: TemplateStringsArray;
    expressions: number | string | Function(props) => number | string;
    predicate?: Predicate<Props>;
}

Returns:

  • styleDefinitions: StyleDefinition[];

Example:

import { styleCollector } from 'trousers';

const styles = styleCollector('button')
    .element``
    .modifier(...)``;

styles.get();

useStyles() alias useTrousers()

React Hook responsbile for evaluating the supplied styles, attaching them to the document head and returning all active classes for the current state.

Arguments:

  • styleCollector: StyleCollector
  • props?: Object
  • state?: Object

Returns:

  • className: string

Example:

import React from 'react';
import { styleCollector, useStyles } from 'trousers';

const styles = styleCollector('button')
    .element``
    .modifier(...)``;

const Button = props => {
    const classNames = useStyles(styles, props);

    return (
        <button className={classNames}>
            Submit
        </button>
    );
};

withStyles alias withTrousers()

A HOC (Higher Order Component) which accepts a component and a trousers style collector. Returns a new component, with the supplied styles rendered and passed down to via a className prop.

Use this HOC in your class components, where hooks (and useStyles) are not available.

Note: Remember to apply the supplied className prop to an element in your components render funciton or your styling wont be applied to your element!

Arguments:

  • Component: React Component
  • styleCollector: StyleCollector

Example:

import React from 'react';
import { styleCollector, withStyles } from 'trousers';

const styles = styleCollector('button')
    .element``
    .modifier(true)``;

class Button {
    render() {
        return (
            // IMPORTANT: apply the className yourself
            <button className={this.props.className}>
                Submit
            </button>
        )
    }
);

export default withStyles(Button, styles);

<ThemeProvider />

Responsible for pushing the supplied theme into React's Context API.

Props:

  • theme: Object

Example:

import React from 'react';
import { ThemeProvider } from 'trousers';

const theme = {
    primaryColor: 'red',
    secondaryColor: 'blue',
};

const App = () => (
    <ThemeProvider theme={theme}>
        {* Every child node will have access to the theme *}
    </ThemeProvider>
);

css

Single style defintion

Arugments:

  • taggedTemplate: TaggedTemplate

Example:

import { css } from 'trousers';

const styles = css`
    background-color: red;
`;

useGlobal()

Mount a single style definition as a global style

Arguments:

  • styleCollector: StyleCollector

Returns

  • clearStyles: Function() => void

Example:

import React, { useEffect } from 'react';
import { css, useGlobal } from 'trousers';

const globalStyles = css`
  @font-face {
    font-family: MyFont;
    src: url('${MyFont}') format('opentype');
  }
`;

const App = () => {
    const [clearStyles] = useGlobal(globalStyles);

    // clearStyles allows you to remove all global styles mounted by that component
    useEffect(() => () => clearStyles(), []);

    return <h1>Welcome to my website!</h1>;
};

ServerStyleRegistry

A style registry for use on the server

Example:

import { ServerStyleRegistry, ServerProvider } from 'trousers';

const registry = new ServerStyleRegistry();
const styleTags = registry.get();

ServerProvider

A context provider which tells Trousers to push styles into the supplied registry, rather than document.head. For use on the server.

Props:

  • registry: SeverStyleRegistry()
  • children: ReactChildren

Example:

import React, { FC, ReactNode } from 'react';
import { renderToString } from 'react-dom/server';

import { ServerStyleRegistry, ServerProvider } from 'trousers';
import App from './';

const registry = new ServerStyleRegistry();

const html = renderToString(
    <ServerProvider registry={registry}>
        <App />
    </ServerProvider>,
);

const styleTags = registry.get();

FAQ

Can't you do this in styled-components and emotion by just creating a new css block instead of only using it in the value.

This can most certainly be done in styled-components and emotion! They are both great libraries, packed with loads of features. Trousers on the otherhand, aims to be a little more simple and oppinionated, it urges you to be deliberate about how styles are defined for particular states so that they can be clearer and more maintainable.

What does this have to do with hooks? Can we not compute the classname from a plain-old JS function?

The reason Trousers is a hook was so it could access (consume) the context from within the library, without exposing that implmentation detail to the user. Otherwise you would have to wrap or access the context manually and pass it into Trousers. There are also plans on leverage hooks more down the line to enable a few new features.

Resources

Tools

You can’t perform that action at this time.