0.7.0 • Published 8 months ago

class-variance-authority v0.7.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
8 months ago

CVA

Introduction

CSS-in-TS libraries such as Stitches and Vanilla Extract are fantastic options for building type-safe UI components; taking away all the worries of class names and StyleSheet composition.

…but CSS-in-TS (or CSS-in-JS) isn't for everyone.

You may need full control over your StyleSheet output. Your job might require you to use a framework such as Tailwind CSS. You might just prefer writing your own CSS.

Creating variants with the "traditional" CSS approach can become an arduous task; manually matching classes to props and manually adding types.

cva aims to take those pain points away, allowing you to focus on the more fun aspects of UI development.

Acknowledgements

  • Stitches (Modulz)
    Huge thanks to the Modulz team for pioneering the variants API movement – your open-source contributions are immensely appreciated
  • clb (Bill Criswell)
    This project originally started out with the intention of merging into the wonderful clb library, but after some discussion with Bill, we felt it was best to go down the route of a separate project.
    I'm so grateful to Bill for sharing his work publicly and for getting me excited about building a type-safe variants API for classes. If you have a moment, please go and star the project on GitHub. Thank you Bill!
  • Vanilla Extract (Seek)

Installation

npm i class-variance-authority

Unfortunately, yes. Originally, the plan was the publish the package as cva, but this name has been taken and marked as a "placeholder". I've reached out to the author and NPM support, but have yet to hear back.

In the meantime, you can always alias the package for your convenience…

Aliasing

  1. Alias the package with npm install

    npm i cva@npm:class-variance-authority
  2. Then import like so:

    import { cva } from "cva";
    
    // …

Tailwind CSS IntelliSense

If you're using the "Tailwind CSS IntelliSense" Visual Studio Code extension, you can enable autocompletion inside cva by adding the following to your settings.json:

{
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ]
}

Getting Started

Disclaimer: Although cva is a tiny library, it's best to use in a SSR/SSG environment – your user probably doesn't need this JavaScript, especially for static components.

Your First Component

To kick things off, let's build a "basic" button component, using cva to handle our variant's classes

Note: Use of Tailwind CSS is optional

// components/button.ts
import { cva } from "class-variance-authority";

const button = cva(["font-semibold", "border", "rounded"], {
  variants: {
    intent: {
      primary: [
        "bg-blue-500",
        "text-white",
        "border-transparent",
        "hover:bg-blue-600",
      ],
      // **or**
      // primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600",
      secondary: [
        "bg-white",
        "text-gray-800",
        "border-gray-400",
        "hover:bg-gray-100",
      ],
    },
    size: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
  },
  compoundVariants: [{ intent: "primary", size: "medium", class: "uppercase" }],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

button();
// => "font-semibold border rounded bg-blue-500 text-white border-transparent hover:bg-blue-600 text-base py-2 px-4 uppercase"

button({ intent: "secondary", size: "small" });
// => "font-semibold border rounded bg-white text-gray-800 border-gray-400 hover:bg-gray-100 text-sm py-1 px-2"

Additional Classes

All cva components provide an optional class prop, which can be used to pass additional classes to the component.

// components/button.ts
import { cva } from "class-variance-authority";

const button = cva(/* … */);

button({ class: "m-4" });
// => "…buttonClasses m-4"

TypeScript Helpers

cva offers the VariantProps helper to extract variant types

// components/button.ts
import type { VariantProps } from "class-variance-authority";
import { cva, cx } from "class-variance-authority";

/**
 * Button
 */
export type ButtonProps = VariantProps<typeof button>;
export const button = cva(/* … */);

Composing Components

Whilst cva doesn't yet offer a built-in method for composing components, it does offer the tools to extend components on your own terms…

For example; two cva components, concatenated together with cx:

// components/card.ts
import type { VariantProps } from "class-variance-authority";
import { cva, cx } from "class-variance-authority";

/**
 * Box
 */
export type BoxProps = VariantProps<typeof box>;
export const box = cva(["box", "box-border"], {
  variants: {
    margin: { 0: "m-0", 2: "m-2", 4: "m-4", 8: "m-8" },
    padding: { 0: "p-0", 2: "p-2", 4: "p-4", 8: "p-8" },
  },
  defaultVariants: {
    margin: 0,
    padding: 0,
  },
});

/**
 * Card
 */
type CardBaseProps = VariantProps<typeof cardBase>;
const cardBase = cva(["card", "border-solid", "border-slate-300", "rounded"], {
  variants: {
    shadow: {
      md: "drop-shadow-md",
      lg: "drop-shadow-lg",
      xl: "drop-shadow-xl",
    },
  },
});

export interface CardProps extends BoxProps, CardBaseProps {}
export const card = ({ margin, padding, shadow }: CardProps = {}) =>
  cx(box({ margin, padding }), cardBase({ shadow }));

API Reference

cva

Builds a cva component

const component = cva("base", options);

Parameters

  1. base: the base class name (string, string[] or null)
  2. options (optional)
    • variants: your variants schema
    • compoundVariants: variants based on a combination of previously defined variants
    • defaultVariants: set default values for previously defined variants.
      note: these default values can be removed completely by setting the variant as null

Returns

A cva component function

cx

Concatenates class names

const className = cx(classes);

Parameters

  • classes: array of classes to be concatenated

Returns

string

Examples

⚠️ Warning: The examples below are purely demonstrative and haven't been tested thoroughly (yet)

/* styles.css */
.button {
  /* */
}

.button--primary {
  /* */
}
.button--secondary {
  /* */
}

.button--small {
  /* */
}
.button--medium {
  /* */
}

.button--primary-small {
  /* */
}
import { cva } from "class-variance-authority";

const button = cva("button", {
  variants: {
    intent: {
      primary: "button--primary",
      secondary: "button--secondary",
    },
    size: {
      small: "button--small",
      medium: "button--medium",
    },
  },
  compoundVariants: [
    { intent: "primary", size: "medium", class: "button--primary-small" },
  ],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

button();
// => "button button--primary button--medium"

button({ intent: "secondary", size: "small" });
// => "button button--secondary button--small"
// button.11ty.js
const { cva } = require("class-variance-authority");

// ⚠️ Disclaimer: Use of Tailwind CSS is optional
const button = cva("button", {
  variants: {
    intent: {
      primary: [
        "bg-blue-500",
        "text-white",
        "border-transparent",
        "hover:bg-blue-600",
      ],
      secondary: [
        "bg-white",
        "text-gray-800",
        "border-gray-400",
        "hover:bg-gray-100",
      ],
    },
    size: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
  },
  compoundVariants: [{ intent: "primary", size: "medium", class: "uppercase" }],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

module.exports = function ({ label, intent, size }) {
  return `<button class="${button({ intent, size })}">${label}</button>`;
};
/* button.css */
.base {
  /* */
}

.primary {
  /* */
}
.secondary {
  /* */
}

.small {
  /* */
}
.medium {
  /* */
}

.primaryMedium {
  /* */
}
// button.tsx
import React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";

import {
  base,
  primary,
  secondary,
  small,
  medium,
  primaryMedium,
} from "./button.css";

const button = cva(base, {
  variants: {
    intent: {
      primary,
      secondary,
    },
    size: {
      small,
      medium,
    },
  },
  compoundVariants: [
    { intent: "primary", size: "medium", class: primaryMedium },
  ],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

export type ButtonProps = VariantProps<typeof button>;

export const Button: React.FC<ButtonProps> = ({ intent, size, ...props }) => (
  <button className={button({ intent, size })} {...props} />
);
// button.tsx
import React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";

// ⚠️ Disclaimer: Use of Tailwind CSS is optional
const button = cva("button", {
  variants: {
    intent: {
      primary: [
        "bg-blue-500",
        "text-white",
        "border-transparent",
        "hover:bg-blue-600",
      ],
      secondary: [
        "bg-white",
        "text-gray-800",
        "border-gray-400",
        "hover:bg-gray-100",
      ],
    },
    size: {
      small: ["text-sm", "py-1", "px-2"],
      medium: ["text-base", "py-2", "px-4"],
    },
  },
  compoundVariants: [{ intent: "primary", size: "medium", class: "uppercase" }],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

export type ButtonProps = VariantProps<typeof button>;

export const Button: React.FC<ButtonProps> = ({ intent, size, ...props }) => (
  <button className={button({ intent, size })} {...props} />
);
<!-- button.svelte -->
<script lang="ts">
  import { cva } from "class-variance-authority";
  import type {VariantProps} from "class-variance-authority";

  const button = cva("button", {
    variants: {
      intent: {
        primary: "button--primary",
        secondary: "button--secondary",
      },
      size: {
        small: "button--small",
        medium: "button--medium",
      },
    },
    compoundVariants: [
      { intent: "primary", size: "medium", class: "button--primary-medium" },
    ],
    defaultVariants: {
      intent: "primary",
      size: "medium",
    },
  });

  type ButtonProps = VariantProps<typeof button>;

  export let intent: ButtonProps["intent"];
  export let size: ButtonProps["size"];
</script>

<button class={button({ intent, size })}><slot /></button>

<style>
  .button { /* … */ }

  .button--primary { /* … */ }
  .button--secondary { /* … */ }

  .button--small { /* … */ }
  .button--medium { /* … */ }

  .button--primary-medium { /* … */ }
</style>
<!-- button.vue -->
<script lang="ts">
import { defineComponent } from "vue";

import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";

const button = cva("button", {
  variants: {
    intent: {
      primary: "button--primary",
      secondary: "button--secondary",
    },
    size: {
      small: "button--small",
      medium: "button--medium",
    },
  },
  compoundVariants: [
    { intent: "primary", size: "medium", class: "button--primary-medium" },
  ],
  defaultVariants: {
    intent: "primary",
    size: "medium",
  },
});

type ButtonProps = VariantProps<typeof button>;

export default defineComponent({
  props: ["intent" as ButtonProps["intent"], "size" as ButtonProps["size"]],
  methods: {
    button,
  },
});
</script>

<template>
  <button :class="button({ intent, size })">
    <slot></slot>
  </button>
</template>

<style>
.button {
  /* … */
}

.button--primary {
  /* … */
}
.button--secondary {
  /* … */
}

.button--small {
  /* … */
}
.button--medium {
  /* … */
}

.button--primary-medium {
  /* … */
}
</style>

Other Use Cases

Although primarily designed for handling class names, at its core, cva is really just a fancy way of managing a string…

const greeter = cva("Good morning!", {
  variants: {
    isLoggedIn: {
      true: "Here's a secret only logged in users can see",
      false: "Log in to find out more…",
    },
  },
  defaultVariants: {
    isLoggedIn: "false",
  },
});

greeter();
// => "Good morning! Log in to find out more…"

greeter({ isLoggedIn: "true" });
// => "Good morning! Here's a secret only logged in users can see"
gaboponentsedit-blog-from-site@codewithhong/utils@mantyke/styles@skylarrr/sky-uibys-containercreate-thoo-appcreate-thoo-app-authds-alfabit@trycreo/next-starter@jarisinc/design-system@c6r/reactfileread-platojetpackai-embedmooglekitsharpcodes-react-form-componentsui-lib-dipairtu-components-2@trycreo/starter@everything-registry/sub-chunk-1332@mznrasil/nextjs-boilerplatemyspaceui-cmpreact-butalertods-basexhttp-ui@bob-obringer/odshackhubb-components@anstack/create-anstackprogress-report-generator-v2@next-devtools/clientbot-test-purpose2@kidsontheyard/kidsontheyard-ui-react-corenextra-docs-demoddreams-design-system@lil-lab-example/uiamigo-design-systemdinglo-io1dinglo-io2dinglo-io3dinglo-io4dinglo-io5dinglo-io6dinglo-io7@lezztable/reactpivot-uirussiaui@million23/nextjsgerard-shadcn-testfusillo@konkat/konkat-uibys-container-ui2srmg-ui-component-library-testsmart-blog-generator@oc-wh/react-dialogskeleton-ui-1wisedelivery-react-component-libraryhealth-care-admin-web-vitetest-reactt-uinova-textesz-ui@hoppla/hoppla-uipgforsta-design-systempkm-componentssystem-equatordev-react-buttonnext14-ts-shadcn-minimalsetdls-testing2package-barims-uiimobilairiko-uihsnpackageimput-cmsipao-uiintelops-component-libraryinfiuilibinker-componentistanbul-widgetismael3s-hellohyperverge-intern-ui-libraryhyperverge-intern-ui-library-2hyperverge-sde-ui-libraryhound-uihotelier-libhotelier-lib-testhyfgnlutzxbfkhvnicep-uihoney-moneyjtdwn-editor-corejsui-components-reactjobiqo-cl-vanillajmdev-studio-libraryjonastoks-library-uijotdown-packagekh-wcvkeenkiso-utilskozut-vite-uikodmq-ui
0.7.1-canary.0

8 months ago

0.7.1-canary.2

8 months ago

0.7.0

9 months ago

0.7.1-canary.1

8 months ago

0.6.1

10 months ago

0.6.0-canary.0

12 months ago

0.5.3-canary.0

12 months ago

0.5.3

12 months ago

0.6.0

12 months ago

0.5.1-canary.0

1 year ago

0.5.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.4.0

1 year ago

0.3.0

1 year ago

0.3.0-canary.0

1 year ago

0.2.4

2 years ago

0.2.3

2 years ago

0.2.2-canary.1

2 years ago

0.2.2

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.2.1-canary.1

2 years ago

0.1.0

2 years ago

0.0.1

2 years ago

0.0.0

2 years ago