Giter VIP home page Giter VIP logo

trousers's Introduction

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

Trousers πŸ‘–

min npm Downloads per month

React components are more stylish with Trousers!

Try it here

Trousers is a hooks-first CSS-in-JS library, designed to help developers author React apps with performant and semantic CSS. It is heavily influenced by the conventions introduced by BEM, borrowing the concept of Blocks (the component), Elements (children nodes) and Modifiers (styles as a function of state). Through this API, Trousers encourages semantic organisation of styles without inadvertently increasing the runtime implications often associated with CSS-in-JS libraries.

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

Table of Contents

Get started ⚑️

Installation

npm install --save trousers or yarn add trousers

Basic example

A basic purple button:

import { css, useStyles } from '@trousers/core';

const Button = props => {
    const className = useStyles(css`
        background-color: rebeccapurple;
        color: white;
    `);

    return <button className={className}>{props.children}</button>;
};

export default Button;

Complete example

A themed button with a primary variant:

app/components/button.jsx

import { useStyles } from '@trousers/core';
import styleCollector from '@trousers/collector';

const styles = props => 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('primary', props.primary)`
        background-color: #f95b5b;
        color: #ffffff;

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

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

    return <button className={buttonClassNames}>{props.children}</button>;
};

export default Button;

app/MyApp.jsx

import { ThemeProvider } from '@trousers/theme';

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 🧠

Components often require many variations and states to be flexible and truly reusable. Think about a simple Button, it can have variations like primary, secondary, subtle and each variation has it's own states, clicked, hover, loading. But with modern CSS-in-JS libraries it can be hard to represent these variations and states in a way that makes sense to everyone and is repeatable without having to memorise specific syntax.

Consider this example:

const Button = styled.button`
    background: ${props => (props.primary ? 'palevioletred' : 'white')};
    color: ${props => (props.primary ? 'white' : 'palevioletred')};
    margin: 1em;
    padding: 0.25em 1em;
    border: 2px solid palevioletred;
`;

We have a button with two variants, default and primary. Functionally it works, but semantically it's really hard to see at a glance what color will be applied when it's primary. How would we extend this further, if say, we wanted primary buttons to have a disabled state?

What's more, for every permutation of props, a new class will be created and attached to the <head>. Every class that is created incurs additional runtime cost, this can grow exponentially if you're not careful, resulting in a combinatorial explosion of classnames πŸ’₯. Consider a component with 3 variants and 3 possible states, that is 3 x 3 = 9, 9 eventual classes generated for one component. It doesn't scale, but we could take another approach:

const Button = styled.button`
  background: white;
  color: palevioletred;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;

  ${props => props.primary && css`
    background: palevioletred;
    color: white;
  `}
}

Now that's more like it! This can be extended and scales to many variations and states.

I think @MadeByMike articulated this perfectly in: CSS Architecture for Modern JavaScript Applications πŸ‘Œ

BEM gave semantic meaning to classnames and one of the biggest unseen values in this was we could immediately transfer our intentions to other developers across teams and even projects. If you know BEM you can look at a classname, e.g. button--state-success and immediately recognise this as a modifier for a button class.

But there's still a problem, this syntax has to be memorised and there's nothing stopping you from falling back into the previous example. This is where an abstraction can protect us and scale that knowledge across your codebase. This is where Trousers can help πŸŽ‰...

Using our style collector you can express these variants and states like so:

import { styleCollector, useStyles } from 'trousers';

const styles = styleCollector('button').element`
        // Base styles applied to all buttons
        color: white;
    `.modifier('primary', props => !!props.primary)`
        // A modifier for the primary variant
        color: black;
    `.modifier('secondary', props => !!props.secondary)`
        color: blue;
    `.modifier('subtle', props => !!props.subtle)`
        color: blue;
    `;

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

    return <button className={buttonClassNames}>{props.children}</button>;
};

export default Button;

In this scenario, Trousers will only ever mount 3 classes to the <head> and toggle them on and off using the predicates provided to the style collector. It will only ever mount what it needs so, if a subtle button is never used you wont pay the run-time cost of processing and mounting those styles.

Under the hood, style collectors are simply an array of styles. This opens the door to a lot of possibilites because it is possible to create your own style collectors that suit your specific needs. What if you want a state machine style collector? Or a style collector that accepts objects instead of template literals? You can simply define one and pass it straight into Trouses 😲!

Features ✨

Hooks-first 🎣

Hooks are a (relatively) 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));

    return <button className={buttonClassNames}>{props.children}</button>;
};

Composable API πŸ—

Trousers is based on a monorepo architecture, meaning that the internals of the repo have been decomposed into a group of smaller stand-alone packages. This allows you to opt-in to features such as SSR, Theming and BEM-style collectors. Doing this will reduce your bundlesizes and tailor (lol) trousers to suit your application.

Otherwise you can use the base Trousers package which is an out-of-the-box composition for the above.

CSS Prop πŸ‘©β€πŸŽ€

Trousers supports a css prop, similar to that of emotion and styled-components! This is handy when you want to skip the boilerplate of declaring useStyles hooks in your components and instead just pass style collectors directly to the components you wish to style.

Just remember to import jsx and set it as the pragma at the top of the file.

For example...

/** @jsx jsx */
import { jsx, css } from '@trousers/core';

const Button = ({ children }) => (
    <button
        css={css`
            background-color: #b3cde8;
            color: white;
        `}
    >
        {children}
    </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/theme';

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 = props => styleCollector('button').element`
        background-color: ${theme => theme.secondaryColor};
    `.modifier(props.primary)`
        background-color: ${theme => theme.primaryColor};
    `.modifier(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, useGlobals:

import React, { useEffect } from 'react';
import { css, useGlobals } from '@trousers/core';

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

const App = () => {
    useGlobals(globalStyles);

    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/server';
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();

Object notation support πŸ“š

If template strings aren't for you it's easy to leverage css as objects.

Simply pass a css compliant object to Trousers.

const classNames = useStyles({
    backgroundColor: 'blue';
    color: 'white'
})

Custom Style Collectors πŸ•Ί

Don't like the style collector API? That's fine, Trousers lets you supply your own! Under the hood a style collector is simply an array of objects which makes it easy to say, build a state-machine style collector.

For example a minimal style collector could look something like this:

import { StyleDefinition, Predicate, Expression } from '@trousers/utils';
import { toHash } from '@trousers/hash';

export default function myStyleCollector(styles) {
    return {
        get: () => [
            {
                styles,
                hash: toHash(styles.toString()).toString(),
                predicate: true,
                name: 'custom_prefix--',
            },
        ],
    };
}

and use it like this:

import myStyleCollector from './my-style-collector';

const styles = myStyleCollector(`
 button {
     color: red;
 }
`)

...

useStyle(styles);

API Reference πŸ“–

Trousers a monorepo made up smaller packages, each with their own responsibilities. Please see respective packages for API information.

FAQ πŸ€·β€β™€οΈ

Can't you do this in styled-components and emotion?

This can most certainly be done in styled-components and emotion! They are both great libraries, packed with loads of features. Trousers on the other hand, aims to be a little more simple and opinionated, 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 implementation 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.

trousers's People

Contributors

danieldelcore avatar dependabot[bot] avatar github-actions[bot] avatar monkeywithacupcake avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

trousers's Issues

Theming with new isMounted flag?

Looks like multiple themes aren’t working with the new isMounted flag.

The problem is that the flag will be set and won’t be able to renter the interpolate and mount logic.

The fix, check if the mounted theme is different for the current one

CSS prop

Allow Trousers to be used with a CSS prop.
This would be a great transport mechanism for a compile-time variant.

But we'll need to change some things, namely the styleCollector...

From this:

import { styleCollector, useStyles } from 'trousers';

const styles = styleCollector('button').element`
        background-color: red;
    `.modifier('primary', props => !!props.primary)`
        background-color: blue;
    `;

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

    return <button className={buttonClassNames}>{props.children}</button>;
};

to this:

import { styleCollector } from 'trousers';

const Button = props => {
    return <button css={
        styleCollector('button').element`
            background-color: red;
       `.modifier('primary', !!props.primary)`
           background-color: blue;
       `;
    }>{props.children}</button>;
};

Orrrrr, if we wanted to share style collectors

import { styleCollector } from 'trousers';

const buttonStyles = (props, state) => styleCollector('button').element`
            background-color: red;
       `.modifier('primary', !!props.primary)`
           background-color: blue;
       `;

const Button = props => {
    return <button css={buttonStyles(props, state)}>{props.children}</button>;
};

Ensure @trousers/macro can handle dynamic interpolations

Exploring some of the different options relating to dynamic interpolations...

Base case:

import { css } from './macro';
const foo = () => 'bar';

css('Button', { color: foo() });

Option 1: Passing interpolations to the style tag

/** @jsx jsx */
import { css, jsx } from '@trousers/macro/runtime';
const foo = () => 'bar';

const styles = css('Button', {
    '.Button-2561700995': 'color: var(--interpolation1);',
})

<button css={styles} styles={{ interpolation1: foo() }} />

Option 2: Passing interpolations via the collector

This would require that we attach interpolations to the inner element via the style attribute

/** @jsx jsx */
import { css, jsx } from '@trousers/macro/runtime';
const foo = () => 'bar';

css('Button', { '.Button-2561700995': 'color: var(--interpolation1);' })
  .interpolations({ interpolation1: foo() });

<button css={styles} />

Inside the jsx pragma we would need to:

  1. Mount the styles
  2. Take interpolations and attach them to the inner element's style tag
    OR
  3. mount them like a theme (taking note that as the values change, new classes will need to be mounted)

It's monorepo time, baby!

Lets decompose trousers into a monorepo!

  • pull in bolt
  • pull in changesets
  • deploy to npm

Project structure

  • Core: the essentials

  • Style-collector: The styleCollector

  • Babel plugin: todo

  • something

  • ensure there's a trousers package which is backwards compatible with the previous version

would be nice to register multiple globals at a time

useGlobal([reset, base, etc])

or

useGlobal(reset, base, etc)

For cases where you might want to "compose" your globals and separate them in some way.
Another added benefit would be that you would only ever have/need one clearStyles function.

Add keyframes to the collector

Idea for post v4

keyframe() collector

Would look like this

const css('Styles', { animation: SlideIn })
 .keyframe('SlideIn', {
  from {
    transform: translateX(0%);
  }

  to {
    transform: translateX(100%);
  }
});

Zero runtime

Could we look into ways to minimise the runtime further?

My initial thought was to use css variables for the theme so styles don't need to be recalculated for every component.

Possible caveats:

  • IE 11 support 😒

Create a way to attach global styles

We need some mechanism to allow us to mount global styles, to support @font-face declarations etc.

Something like this πŸ€”

import { global, useGlobalStyle } from 'trousers';

const globalStyle = global`
    @font-face {
        font-family: myFirstFont;
        src: url(sansation_light.woff);
    }
`;

const App = (props) => {
   useGlobalStyle(globalStyle);

   return (
        <div />
   )
}

export defualt App;

Build-time optimisations via Bable-macros

We should look into this as a way to reduce runtime costs!
And set us up for zero-config SSR:

https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros

so using the babel macro we could go from this:

/** @jsx jsx */
import { css, jsx} from '@trousers/core'

const styles = css`
  color: white;
`;

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

    return <button className={buttonClassNames}>{props.children}</button>;
};

export default Button;

to this:

/** @jsx jsx */
import { css, jsx} from '@trousers/macro'

const styles = css`
  color: white;
`;

const Button = props => {
    return <button css={styles}>{props.children}</button>;
};

export default Button;
  • So we'll swap our JSX pragma for one that can support zero runtime
  • Allows us to keep the current API for people who don't want to opt into the CSS prop etc
  • Perform optimisations on the CSS like
    • Removing comments
    • Vendor prefixing
    • Other cool stuff?

Sensibly named hooks

For folks who want to use a library with a ridiculous name, but don't want that name spread throughout their codebase we should provide:

useTrousers => useStyles

trousers() => styleCollector()

useGlobal duplicates styles on re-render

If useGlobal is called twice, it has no way of detecting if a global has been mounted or not and will mount it again, causing duplicates.

To fix:
ensure all globals have a hash so that we can determine if the global has been mounted already.

and also ensure docs, tests, and storybooks are updated to match

Allow names to be attached to modifiers

Feedback from @theKashey

image

Allow users to supply a name to modifiers to make them debugger friendly.

const styles = styleCollector('button')
  .element``
  .modifier('primary', props => props.primary)`
    color: red;
  `;

the resulting className would end up looking something like button__40241422510--primary-123131

πŸ€”Perhaps we could omit the hash if a name is provided

Ensure styles aren't remounted when the page has already been server rendered

When a page has been server rendered and hydrated on the client side, ensure that the styles aren't mounted to the page again.

We will have to:

  • check for a style tag with the data-trousers attribute.
  • make sure that has has been mounted
  • avoid mounting styles if it already exists in the style tag.

or fine some other clever way around it

CSS with different interpolations not mounted

Having an instance of <Grid> and <Grid column={2}> on the page at the same time does not remount the class to the head of the document. Could be something to do with the hashing logic.

/** @jsx jsx */
import { FC, ReactNode } from 'react';

import { css, jsx } from '@trousers/core';

export const Grid = ({ columns = 3, children }) => (
    <div
        css={css`
            display: grid;
            grid-template-columns: repeat(${columns}, 1fr);
            gap: 1.5rem;
            width: 100%;
        `}
    >
        {children}
    </div>
);

Trousers v4 - An idea

Improvements βœ…

  • Leaning into the CSS prop
  • Zero config SSR becomes possible
  • Modifier predicates are passed into the underlying element
  • No longer dependant on hooks
  • Dynamic properties can be applied to css vars to avoid remounting styles
  • Less dependency on React

Challenges ❌

  • Extending proptypes in typescript for basic elements
- import { useStyles } from '@trousers/core';
- import styleCollector from '@trousers/collector';

+ /** @jsx jsx */
+ import css from '@trousers/core';
+ import jsx from '@trousers/react';

- const styles = props => styleCollector('button')
-    .element`
-        background-color: ${theme => theme.backgroundColor};
-    `
-   .modifier('primary', props.primary)`
-        background-color: #f95b5b;
-    `;

+ const styles = css('button', `
+        background-color: ${theme => theme.backgroundColor};
+   `)
+    .modifier('primary')`
+        background-color: #f95b5b;
+   `
+    .modifier('disabled')`
+        background-color: red;
+   `;

const Button = props => {
-    const buttonClassNames = useStyles(styles(props));

    return <button 
-      className={buttonClassNames}
+      css={styles}
+      primary={props.primary}
    >
      {props.children}
    </button>;
};

export default Button;

Themes should now be mounted as classnames of css vars. Rather than depending on costly react context, we'll apply themes via a classname and css vars. The themes will be mounted to the head just like any other style.

For example:

const theme = {
 primary: 'red',
 secondary: 'blue,
};

is mounted as:

.theme-somehash {
 --primary: red;
 --secondary: blue;
}

which is applied to a button like so:

/** @jsx jsx */
import css from '@trousers/core';
import jsx from '@trousers/react';
import createTheme from '@trousers/theme';

+ const theme = createTheme({
+   primary: 'red',
+   secondary: 'blue,
+});

const styles = css('button', `
-        background-color: ${theme => theme.backgroundColor};
+        background-color: var(--theme-secondary);
   `)
   .modifier('primary')`
+        background-color: var(--theme-primary);
  `;

const Button = props => {
    return <button 
      css={styles}
      primary={props.primary}
+      theme={theme}
    >
      {props.children}
    </button>;
};

export default Button;

CSS object notation support

Add support for css object notation to style collectors

const fooStyles = styleCollector('foo').element({ backgroundColor: 'red' });

Dynamic expressions that return different values are hashed the same way

function getGap({ space }: Theme) {
        const column = columnGap ? space[columnGap] : space[gap];
        const row = rowGap ? space[rowGap] : space[gap];

        return `${row} ${column}`;
    }

    return (
        <div
            css={css<Theme>`
                display: grid;
                grid-template-columns: repeat(${columns}, 1fr);
                gap: ${getGap};
                width: 100%;
            `}
        >
            {children}
        </div>
    );

Map dynamic variables to css variables

... to avoid remounting the same styles with dynamic expressions multiple times...

Inspired by https://compiledcssinjs.com/

import React from 'react';
import { CC, CS } from '@compiled/css-in-js';
export const EmphasisText = (props) => {
  const color = props.massive ? '#00B8D9' : '#36B37E';
  return (
    <CC>
      <CS hash="vmwvfg">
        {[
          '.cc-vmwvfg{color:var(--var-1ylxx6h);text-transform:uppercase;font-weight:600}',
        ]}
      </CS>
      <span className="cc-vmwvfg" style={{ '--var-1ylxx6h': color }}>
        {props.children}
      </span>
    </CC>
  );
};

Pass state into predicates

Currently we have no way to trigger modifiers with component state.

Pass state into useTrousers and pass it into predicates.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.