[]previous and next page

Variants without a dependency

The shadcn/ui model builds component variants with class-variance-authority and joins them with cn().

For agentsUpdated

The shadcn/ui model builds component variants with class-variance-authority and joins them with cn(). Neither package is needed: the helper below has the same shape (base, variants, defaultVariants, compoundVariants) in about 40 lines, and its result is a string that class accepts.

src/lib/variants.tsts
export type ClassValue =
  | string
  | number
  | bigint
  | null
  | undefined
  | false
  | readonly ClassValue[]
  | { readonly [name: string]: unknown };

/** Joins class values: strings and numbers are kept, falsy values dropped, objects contribute truthy keys. */
export function cn(...inputs: readonly ClassValue[]): string {
  const out: string[] = [];
  const add = (value: ClassValue): void => {
    if (value === null || value === undefined || value === false || value === "") return;
    if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
      out.push(String(value));
    } else if (Array.isArray(value)) {
      for (const item of value as readonly ClassValue[]) add(item);
    } else {
      for (const [name, enabled] of Object.entries(value)) if (enabled) out.push(name);
    }
  };
  for (const input of inputs) add(input);
  return out.join(" ");
}

type VariantTable = Record<string, Record<string, string>>;
type Choice<V extends VariantTable> = { readonly [K in keyof V]?: Extract<keyof V[K], string> | undefined };

export interface VariantsConfig<V extends VariantTable> {
  readonly variants: V;
  readonly defaultVariants?: Choice<V>;
  readonly compoundVariants?: ReadonlyArray<Choice<V> & { readonly class: string }>;
}

/** The selection a caller passes: one key per variant, plus extra classes. */
export type VariantProps<V extends VariantTable> = Choice<V> & { readonly class?: ClassValue };

/** A `cva`-shaped helper: returns a function from a selection to a class string. */
export function variants<V extends VariantTable>(
  base: ClassValue,
  config: VariantsConfig<V>,
): (selection?: VariantProps<V>) => string {
  const table: VariantTable = config.variants;
  return (selection = {} as VariantProps<V>) => {
    const asked: Record<string, unknown> = selection;
    const defaults: Record<string, unknown> = config.defaultVariants ?? {};
    const chosen: Record<string, string | undefined> = {};
    const parts: ClassValue[] = [base];
    for (const name of Object.keys(table)) {
      const key = (asked[name] ?? defaults[name]) as string | undefined;
      chosen[name] = key;
      if (key !== undefined) parts.push(table[name]?.[key]);
    }
    for (const compound of config.compoundVariants ?? []) {
      const rule: Record<string, unknown> = compound;
      const matches = Object.keys(table).every((name) => rule[name] === undefined || rule[name] === chosen[name]);
      if (matches) parts.push(compound.class);
    }
    parts.push(selection.class);
    return cn(parts);
  };
}
src/button.tsxtsx
import { variants } from "./lib/variants";

export const button = variants(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-slate-900 text-white hover:bg-slate-900/90",
        destructive: "bg-red-600 text-white hover:bg-red-600/90",
        outline: "border border-slate-200 bg-white hover:bg-slate-100",
      },
      size: { default: "h-9 px-4", sm: "h-8 px-3 text-xs", lg: "h-10 px-8" },
    },
    defaultVariants: { variant: "default", size: "default" },
    compoundVariants: [{ variant: "outline", size: "sm", class: "border-dashed" }],
  },
);

export type ButtonProps = Parameters<typeof button>[0] & { children?: string };

export function Button(props: ButtonProps) {
  return (
    <button
      type="button"
      class={button({
        variant: props.variant,
        size: props.size,
        ...(props.class === undefined ? {} : { class: props.class }),
      })}
    >
      {props.children}
    </button>
  );
}

export function Buttons() {
  return (
    <>
      <Button>Save</Button>
      <Button variant="destructive" size="lg">
        Remove
      </Button>
    </>
  );
}