@gentleduck/variants
Version:
A package for creating variants of components, providing a simple and efficient way to create variants of components.
196 lines • 6.75 kB
TypeScript
//#region src/variants.types.d.ts
/**
* Maps each variant key to a selected value or array of values.
*
* @template TVariants - A mapping of variant names to their possible class mappings.
* @example
* ```ts
* type ButtonVariants = {
* size: { sm: string; lg: string },
* intent: { primary: string; danger: string }
* }
*
* // Accepts either a single key or an array of keys for each variant
* const params: VariantParams<ButtonVariants> = {
* size: ['sm', 'lg'],
* intent: 'primary'
* }
* ```
*/
type VariantParams<TVariants extends Record<string, Record<string, string | string[]>>> = { [K in keyof TVariants]?: keyof TVariants[K] | Array<keyof TVariants[K]> };
/**
* Configuration for creating a CVA (Class Variance Authority) function.
*
* @template TVariants - A mapping of variant names to their class mappings.
*
* @property {TVariants} variants
* The core variant definitions. Each variant key maps to an object whose keys
* are variant names and values are class strings or arrays of class strings.
*
* @property {VariantParams<TVariants>} [defaultVariants]
* Default selections applied when the user does not supply a value for a variant.
*
* @property {Array<VariantParams<TVariants> & { class?: ClassValue; className?: ClassValue }>} [compoundVariants]
* Array of objects that specify additional `class` or `className` entries when
* multiple variant keys match simultaneously. Each object’s own keys correspond
* to variant names (or arrays of variant names) indicating the match conditions.
*
* @example
* ```ts
* const options: VariantsOptions<{
* size: { sm: string; lg: string },
* tone: { muted: string; loud: string }
* }> = {
* variants: {
* size: { sm: 'text-sm', lg: 'text-lg' },
* tone: { muted: 'opacity-50', loud: 'opacity-100' }
* },
* defaultVariants: { size: 'sm', tone: 'muted' },
* compoundVariants: [
* {
* size: 'lg',
* tone: ['loud', 'muted'],
* className: 'font-bold'
* }
* ]
* }
* ```
*/
interface VariantsOptions<TVariants extends Record<string, Record<string, string | string[]>>> {
variants: TVariants;
defaultVariants?: VariantParams<TVariants>;
compoundVariants?: Array<VariantParams<TVariants> & {
class?: ClassValue;
className?: ClassValue;
}>;
}
/**
* Props accepted by a CVA-generated function: variant selections
* plus optional `class` or `className` to append custom classes.
*
* @template TVariants - A mapping of variant names to their class mappings.
*
* @example
* ```ts
* const props: CvaProps<{
* color: { red: string; blue: string },
* size: { sm: string; lg: string }
* }> = {
* color: 'blue',
* size: 'lg',
* className: ['my-custom-class', { 'is-active': true }]
* }
* ```
*/
type CvaProps<TVariants extends Record<string, Record<string, string | string[]>>> = VariantParams<TVariants> & {
className?: ClassValue;
class?: ClassValue;
};
/**
* Removes an array type from a union type.
* Used to exclude `class` and `className` from `VariantProps`.
* @template T - A union type.
* @example
* ```ts
* type Props = { class?: string; className?: string }
* type PropsWithoutArray = RemoveArray<Props>
* // => { class?: string; className?: string }
* ```
*/
type RemoveArray<T> = T extends any[] ? never : T;
/**
* Extracts only the variant-related props from a CVA function’s signature,
* omitting `class` and `className`.
*
* @template T - A function type returned by `cva(...)`.
*
* @example
* ```ts
* declare const button: (props?: {
* size?: 'sm' | 'lg',
* intent?: 'primary' | 'danger',
* className?: string
* }) => string;
*
* type ButtonVariantOnly = VariantProps<typeof button>
* // => { size: 'sm' | 'lg'; intent: 'primary' | 'danger' }
* ```
*/
type VariantProps<T> = T extends ((props?: infer P) => string) ? { [K in keyof P as K extends 'class' | 'className' ? never : K]: RemoveArray<P[K]> } : never;
/**
* A dictionary mapping CSS class names to boolean flags.
* Useful for conditional inclusion: `{ 'text-bold': isActive }`.
*/
type ClassDictionary = Record<string, boolean | undefined>;
/** An array of class values (nested arrays, strings, dictionaries). */
type ClassArray = ClassValue[];
/**
* Permitted inputs for class names:
* - `string` or `number` (split on whitespace)
* - `boolean` (included if `true`)
* - `ClassDictionary` for conditional keys
* - `ClassArray` for nested lists
*
* @example
* ```ts
* const input: ClassValue = [
* 'px-4',
* { 'bg-red-500': isError },
* ['hover:bg-red-600', ['active:scale-95']]
* ]
* ```
*/
type ClassValue = string | number | boolean | ClassDictionary | ClassArray;
//#endregion
//#region src/variants.d.ts
/**
* Creates a Class Variance Authority (CVA) function for composing class names
* based on a base string, variants, defaultVariants, and compoundVariants.
*
* Supports two call signatures:
* - `cva(base: string, options: VariantsOptions<TVariants>)`
* - `cva(options: VariantsOptions<TVariants> & { base: string })`
*
* @template TVariants
* A record mapping variant keys to a record of allowed values and their classes.
*
* @param {string | (VariantsOptions<TVariants> & { base?: string })} baseOrOptions
* Either the base class string, or an options object including `base`.
* @param {VariantsOptions<TVariants>} [maybeOptions]
* The options object when using the two-arg signature.
*
* @returns {(props?: CvaProps<TVariants>) => string}
* A function that, given variant props and optional `class`/`className`, returns
* the deduplicated, memoized className string.
*
* @example
* ```ts
* const button = cva('btn px-4 py-2', {
* variants: {
* intent: { primary: 'bg-blue-500 text-white', danger: 'bg-red-500' },
* size: { sm: 'text-sm', lg: 'text-lg' },
* },
* defaultVariants: { intent: 'primary', size: 'sm' },
* compoundVariants: [
* {
* intent: ['primary','danger'],
* size: 'lg',
* className: 'uppercase',
* },
* ],
* })
*
* // uses defaults + compound match
* button()
* // => 'btn px-4 py-2 bg-blue-500 text-white text-sm uppercase'
*
* // overrides size + adds custom classes
* button({ size: 'lg', class: ['mt-4','shadow'] })
* // => 'btn px-4 py-2 bg-blue-500 text-white text-lg uppercase mt-4 shadow'
* ```
*/
declare function cva<TVariants extends Record<string, Record<string, string | string[]>>>(baseOrOptions: string | (VariantsOptions<TVariants> & {
base?: string;
}), maybeOptions?: VariantsOptions<TVariants>): (props?: CvaProps<TVariants>) => string;
//#endregion
export { ClassArray, ClassDictionary, ClassValue, CvaProps, VariantParams, VariantProps, VariantsOptions, cva };