Single function to create, manage, compose variants, for any CSS-in-JS libraries.

Overview

Build-variants

Single function to create, manage, compose variants, for any CSS-in-JS libraries.

Motivation

Before diving into the implementation details, you may want to read about design considerations and motivation.

Installation

npm install build-variants

Prerequisites

Typescript is not mandatory but highly recommended. Build-variants leverages a lot on Typescript generics and inference to provide types checking at every level.

How to use

Intanciate build-variants

In order to use build-variants with any CSS-in-JS librairies, build-variants does not provide a CSS interface by default, meaning that your styles objects can be anything.

To provide types checking for styles, you need to pass a type/interface to the build-variants' newBuildVariants function.

Let's take an example with styled-components:

import { newBuildVariants } from 'build-variants'
import { CSSObject } from 'styled-components'

/**
 * Create a build-variants instance, typed to use styled-components's `CSSObject`s.
 */
export function buildVariants<TProps extends object>(props: TProps) {
  return newBuildVariants<TProps, CSSObject>(props)
}

Note that you can use the interface you want. Consider using React.CSSProperties if you are doing raw React styles or your custom object definition:

import { newBuildVariants } from 'build-variants'

// build-variants typings will only tolerate styles with color and background properties!
interface IMyStyles {
  color: string
  background: string
}

/**
 * Create a build-variants instance, to use Partial<IMyStyles>
 */
export function buildVariants<TProps extends object>(props: TProps) {
  return newBuildVariants<TProps, Partial<IMyStyles>>(props)
}

Decorate your components

Now you can use your buildVariants function to build styles objects that will be passed to your styled function - most of the time.

import { buildVariants } from 'path/to/buildVariants'

// Use here styled-components but you can used any CSS-in-JS library you want
import { styled } from 'styled-components'

// Define the interface of the props used by your component
interface Props {
  // Define a 'private' property for the button font color
  _color?: 'default' | 'primary' | 'secondary'

  // Define a 'private' property for the button background
  _background?: 'default' | 'primary' | 'secondary'

  // Define a 'private' property for font variants.
  // This is an array, meaning that you can apply several values at once.
  _font?: Array<'default' | 'bold' | 'italic'>,

  // Define a 'private' property for a disabled state that is a boolean
  _disabled?: boolean

  // Finally, define a "public" variant
  type?: 'default' | 'primary' | 'secondary'
}

// Style a div component by using styled-components here.
const Div = styled.div<Props>(props => {
  // Get a new instance of build-variant.
  // Note that we use here `buildVariants()` defined in step 1 to be able to write
  // CSS styles as styled-components' CSS objects.
  return buildVariants(props)
    // Add some CSS.
    .css({
      background: 'white'
    })

    // Add more CSS.
    // You can add as many CSS blocks as you want.
    .css({
      '> button': {
        all: 'unset'
      }
    })

    // Implement CSS for each case of the color variant.
    // Everything is typed checked here. You have to implement all cases of the
    // union value.
    // Note that because _color is optional, we have to default on a default value,
    // here 'default', which is the first value of the union.
    .variant('_color', props._color || 'default', {
      default: {
        // No color for the default case, it will be inherited from a parent
      },

      primary: {
        color: 'white'
      },

      secondary: {
        color: 'black'
      }
    })

    // Same thing with the background variant
    .variant('_background', props._background || 'default', {
      default: {
        // No background override.
        // As we have define a background in the first CSS block, we should have
        // a background: white at the end.
      },

      primary: {
        background: 'blue'
      },

      secondary: {
        background: 'white'
      }
    })

    // Same thing with the font variant.
    // Note that we use `variant*s*` to manipulate an array of unions.
    .variants('_font', props._font || [], {
      default: {
        // Inherits from the parent
      },

      bold: {
        fontWeight: 'bold'
      },

      italic: {
        fontStyle: 'italic'
      }
    })

    // Same thing with the disabled variant which is a boolean.
    // Therefore we need to define the true and false cases.
    .variant('_disabled', props._disabled || [], {
      true: {
        background: 'silver'
      },

      false: {
        // Nothing is not disabled
      }
    }, {
      // When the button is disabled, we want that the background:silver takes
      // the precedence over all other backgrounds rules.
      // So you can use the `weight` option to ponderate your CSS definition(s).
      // Weight is also available for variant(s), compoundVariant(s) and if blocks.
      weight: 10
    })

    // You can conditionate any CSS or variant definition by using `if()` block.
    .if(
      // Implement the predicate function here to have a pink color in your button
      true    // OR `props.variants?.include('fancy') === true,
      builder => {
        return builder
          .css({
            color: 'pink'
          })
          .end()
      }
    )

    // The nice trick with `if` is that variants will be automatically "skipped"
    // from compound variants when being disabled.
    // For example here, the color variant will not be applyed if used into
    // compoundVariant (see below).
    .if(
      false,
      builder => {
        return builder
          .variant('_color', props._color || 'default', {
             // ...
          }
          .end()
      }
    )

    // Now, compose with your 'private' variants
    .compoundVariant('type', props.type || 'default', {
      // When composing, we get a new instance of the builder to get existing
      // private variants definitions.
      // Final `end()` function merges all CSS definitions get from the composition.
      // Here we don't want to style the default case, so we directly call the
      // end function.
      default: builder.end()

      // Here we define the type=primary variant from existing color, background
      // and font variants.
      // In this example, we use two definitions for the font variant. So the font
      // will be bold and italic.
      primary: builder
        .get('_color', 'primary')
        .get('_background', 'primary')
        .get('_font', ['bold', 'italic'])
        .end(),

      // In the same way, compose to define type=secondary variant.
      secondary: builder
        .get('_color', 'secondary')
        .get('_background', 'secondary')
        .get('_font', ['bold', 'italic'])

        // Not recommanded but you have the full API of the builder available here.
        // So you can create new CSS or variant(s) of event compoundVariant(s) in this
        // compoundVariant definition!
        // .css({
        //   background: 'white'
        // })

        .end()
    })

    // You can also compose with an array of compoundVariant by using compoundVariants:
    // .compoundVariants('types', props.types || [], {
    //   ...
    // })

    // If you have some issues and unexpected CSS applied, you may want debug things
    // so you can use `debug()` function that will log props, variants, CSS parts and
    // final merged CSS object.
    .debug()

    // Finally, merge all CSS definitions and variants.
    // End function will return a CSS object.
    .end()
})

// Create a component and render a disabled "primary" button.
function ButtonComponent() {
  return (
    <Div type="primary" disabled>
      <button>Button</button>
    </Div>
  )
}

/* CSS is going to be:

{
  // Get from the first CSS block
  '> button': {
    all: 'unset'
  },

  // Get the background set in the disabled variant. And because we added a weight
  // option, the background:silver takes the precedence over backgrounds set in
  // the primary variant (blue) and in the first CSS block (white).
  background: 'silver',

  // Get from the primary font variant, two variants applyed at the same time
  fontWeight: 'bold',
  fontStyle: 'italic'

  // Get from the primary color variant
  // (color:pink is not applyed because declared before the primary variant and
  // no weight value has been set)
  color: 'white'
}

*/

Design considerations

About private and public variants

It is a proposal to manage variants with a different level of visibility but there is no obligation at all to follow this pattern.

The interesting approach with private and public variants is that you and your consumers have maximum flexibility.

Consumers are incited to use only public variants and it's recommended to communicate only on "official" and "public" variants but if a custom specific need is required, consumers can use internal variants and customize the component as their needs. It's not recommended but sometimes, pragmatism is a good thing.

About variants versus props interpolation

Variants is something relatively new in CSS-in-JS world that libraries like Stitches have made popular by adding first-class variant API support.

Stitches advocates [variants design instead of props interpolation)(https://stitches.dev/blog/migrating-from-emotion-to-stitches), meaning that variants are defined directly during the styles implementation, by infering definitions. It quicky adds complexity when it comes to extracting those variants in order to reuse them in another contexts. More generally, not having clear interfaces is rarely a good idea.

Build-variants vision is more as:

  1. First, define clear interfaces for your components,
  2. Secondly, implement your interface by defining CSS and variants,
  3. Optionally, compose your variants if you need more high-level behaviors (like a "primary" type that defines a bunch of styles like colors, background and borders for example).

Build-variants provides both, a first-class variant API and props interpolation, allowing to define variants according to props values.

About tokens

Tokens (strings) could be seen as a handly way to create shortcuts for complex styles definitions. But you should consider as well the drawbacks of using simple strings that can't reference the source of the implementation in addition that adding more and more aliases of styles may obfuscate a bit which styles are really applied in the end.

For values, you may want to consider importing directly what you need. If you need a custom set of styles, you can create a function and invoke it directly in styles definition.

function monospaceFontStyles(): CSSObject {
  return {
    fontFamily: 'monospace',
    letterSpacing: '1em',
    fontWeight: 500
  }
}

const StyledTextArea = styled.textarea(props => {
  return buildVariants(props)
    .css({
      color: 'black',
      ...monospaceFontStyles()
    })
    .end()
})

About global variants

Instead of importing a function to inject styles, an another option could be to define kind of global variants used to apply styles without having to import things and without having to define the "same" variant in various places.

To do so, you could make use of the initial the initial buildVariants function that defines the type to use for styles. Just add some variants definitions here and expose an interface that you can use when styling your components.

// Define an ExtendedStyledProps type that will extend TProps with some
// default variants. Here we define a "font" variant.
export type ExtendedStyledProps<TProps extends object> = TProps & {
  font?: 'default' | 'monospace'
}

export function buildVariants<
  TProps extends object,
  // Create a generic that extends ExtendedStyledProps<TProps>, used to type props.
  TExtendedProps extends ExtendedStyledProps<TProps>
>(props: TExtendedProps) {
  return newBuildVariants<TExtendedProps, CSSObject>(props).variant(
    // use the props name as the variant label
    'font',
    // add a default fallback since "font" value can be optional
    props.font || 'default',
    // finally, implement the union values
    {
      default: {
        //
      },

      monospace: {
        fontFamily: 'monospace',
        letterSpacing: '0.1em',
        fontWeight: 5000
      }
    }
  )
}

Now, when styling a component, you can use the extended interface to expose the global variants:

const StyledTextArea = styled.textarea<
  // Type your typearea props as an ExtendedStyledProps<T> interface
  ExtendedStyledProps<HTMLAttributes<HTMLTextAreaElement>>
>(props => {
  return buildVariants(props)
    .css({
      color: 'black'
    })
    .end()
})

// "font" property is now available without having the need to define the variant,
// because already implemented in the `buildVariants` function.
() => <StyledTextArea maxLength={50} font="monospace" value="Hello World" />

Have fun building variants! :)

Comments
  • Extends README.

    Extends README.

    Description of the Change

    Add details about design consideration and motivation.

    Benefits

    Advocating why build-variants is awesome!

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [x] Tests added (N/A)
    opened by cr0cK 1
  • Add a predicate on debug function + 1.3.0

    Add a predicate on debug function + 1.3.0

    Description of the Change

    v1.3.0 (2023-01-04)

    Added

    • Predicate on the debug() function allowing to limit debugging for specific use-cases.

    Updated

    • Rewrite all the README with step-by-step explanations and Codesandboxes examples.

    Benefits

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [ ] Tests added
    opened by cr0cK 0
  • Revamp documentation

    Revamp documentation

    Description of the Change

    Rewrite README and provide CodeSandbox examples.

    Benefits

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [ ] Tests added
    opened by cr0cK 0
  • Support undefined props and private variants applied last

    Support undefined props and private variants applied last

    Description of the Change

    • Prop values passed to variant(s) and compoundVariant(s) are now optional. It allows to not apply default styles or having to create "empty variant" for default cases.

    • Private variants (prop starting by _) now overrides composed variants got from an existing variant declaration.

      For example, let's consider a Button on which the "primary" variant define a white color, if a private variant is defining a different color, it's possible to override the primary color like this:

    <Button variant="primary" _color="red" />
    

    Benefits

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [x] Tests added
    opened by cr0cK 0
  • Add the possibility to compose styles with a local builder instance for css blocks.

    Add the possibility to compose styles with a local builder instance for css blocks.

    Description of the Change

    Add the possibility to compose styles with a local builder instance for css blocks.

    See related test to see the use case.

    Benefits

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [x] Tests added
    opened by cr0cK 0
  • Implement CSS replacements

    Implement CSS replacements

    Description of the Change

    This PR implements CSS replacements that could be useful to implement custom CSS directives, transforms, CSS aliases etc.

    Benefits

    Possible Drawbacks

    Testing Instructions

    Checklist

    • [x] Tests added
    opened by cr0cK 0
  • Add a way to ponderate CSS parts

    Add a way to ponderate CSS parts

    When having same CSS directives in different CSS / variant blocks, it could be useful to have a way to apply them according a weight. That's the goal of this PR.

    Example:

    const css = testBuildVariants({})
      // should be applyied because weighted
      .css(
        {
          color: 'red',
          border: '1px solid black'
        },
        { weight: 10 }
      )
      .css({
        color: 'lime'
      })
      .end()
    
    expect(css).toEqual({
      color: 'red',
      border: '1px solid black'
    })
    
    opened by cr0cK 0
Releases(0.0.1-alpha.1)
Owner
Alexis Mineaud
Alexis Mineaud
Mirrors the functionality of Apollo client's useQuery hook, but with a "query" being any async function rather than GQL statement.

useAsyncQuery Mirrors the functionality of Apollo client's useQuery hook, but with a "query" being any async function rather than GQL statement. Usage

Alasdair McLeay 7 Nov 16, 2022
📚 Compose storybooks for an overview.

"Put all the books in the bookcase." Compose storybooks for an overview. English | 简体中文 ?? CLI bookcase-builder as bb. Background In the monorepo proj

赵東澔 4 Jun 23, 2022
Convert a CSS linear gradient function to expo-linear-gradient props

@bacons/css-to-expo-linear-gradient Demo: snack Convert a CSS linear gradient function to expo-linear-gradient props. Add the package to your npm depe

Evan Bacon 15 Dec 15, 2022
A React application for AddressBook to manage contacts on web. It will use JSON-server to create backend apis.

Available Scripts In the project directory, you can run: npm install To install all related packages npm start Runs the app in the development mode. O

null 1 Jan 10, 2022
An open source application to create, manage and embed contact forms on static/dynamic sites with no code

Formify is an open source application that lets you create, manage and embed contact forms on any site without writing code. This project is done as a

Basharath 37 Dec 26, 2022
nivo provides a rich set of dataviz components, built on top of the awesome d3 and Reactjs libraries

nivo provides supercharged React components to easily build dataviz apps, it's built on top of d3. Several libraries already exist for React d3 integr

Raphaël Benitte 10.9k Dec 31, 2022
Math Magicians is a website for performing basic maths calculations, This website is built using REACT and JavaScript libraries.

Math Magicians Math Magicians is a website for performing basic maths calculations, This website is built using REACT and JavaScript libraries. Screen

Ranjeet Singh 12 Oct 20, 2022
A personal project, made with React, React Native, Typescript and some of the most used front-end libraries.

A personal project, made with React, React Native, Typescript and some of the most used front-end libraries.

Alvaro Junior 1 Jul 23, 2022
This library is a template to generate new angular component libraries.

Open Template Hub - UI Library Template v1 This library is a template to generate new angular component libraries. About the NPM Package Package Insta

Open Template Hub 6 Dec 18, 2022
TryShape is an open-source platform to create shapes of your choice using a simple, easy-to-use interface. You can create banners, circles, polygonal shapes, export them as SVG, PNG, and even as CSS.

Create, Export, Share, and Use any Shapes of your choice. View Demo · Report Bug · Request Feature ?? Introducing TryShape TryShape is an opensource p

TryShape 148 Dec 26, 2022
A React utility belt for function components and higher-order components.

A Note from the Author (acdlite, Oct 25 2018): Hi! I created Recompose about three years ago. About a year after that, I joined the React team. Today,

Andrew Clark 14.8k Jan 4, 2023
use this to replace redux,you can use useActions to change context value and useActions return a mutable function collection

English | 中文 NOTE react-context-mutation is a lighter and more convenient state manager designed for react applications. It aims to replace the Redux

null 19 Feb 22, 2022
a babel plugin that can transform generator function to state machine, which is a ported version of typescript generator transform

Babel Plugin Lite Regenerator intro This babel plugin is a ported version of TypeScript generator transform. It can transform async and generator func

Shi Meng 6 Jul 8, 2022
This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the source code concise.

React Hook Component State This hook allows you to isolate and manage the state within the component, reducing rendering operations and keeping the so

Yongwoo Jung 2 May 15, 2022
A React-Fullstack platform helps to track & compare your Incomes & Expenses transactions in one place via detailed Graphical information to manage your Budget.

Budget Master Track your income and expenses in one place via detailed graphical information to manage your budget. Manage your budget and transaction

Siddharth Singh Bhadoriya 9 Nov 27, 2022
Open Source Website where you can manage your information and artworks of your fursona(s)

MyFursona About this project MyFursona is an open source project where users can manage their artworks, info (such as biography and interests), and lo

MyFursona 23 Jan 7, 2023
💚 Yet another mutli purpose discord bot, allowing you to maintain and manage your discord server with ease.

Jade Jade is once again another mutli purpose bot, allowing you to maintain and manage your discord server with ease. Completely open source or use th

Saige 1 Sep 13, 2022
Single Page Application built using React, Context API and OMDb API.

Movie Search App This project is a React application with functions to search for movies and add movies to favorites using OMDb API. Home Page Favorit

Efecan Pınar 24 Sep 6, 2022
frida runtime (no python required, only a single file),One-click support for ios smashing shell

fd 简要介绍 要是你看不懂中文可以使用chrome翻译功能 frida 运行时(不需要python,只有单一个文件) fd 使用fd前请确认手机上有frida-server a brief introdction English can use chrome translation frida r

null 181 Dec 30, 2022