ui icon indicating copy to clipboard operation
ui copied to clipboard

[feat]: Dynamic add new variants

Open doiya46 opened this issue 1 year ago • 5 comments
trafficstars

Feature description

Problem

Currently some component have fixed variant. For example: const badgeVariants = cva(, const buttonVariants = cva( so I find some solution to add dynamic variant with minimum cost to modify ShadCN UI code.

I don't know if this is a good idea before proceeding to modify the code and create a pull request.

This solution need update any component in /components/ui that used = cva(. So the end-user (developer) cant override variant by update ONLY 1 file custom-vars.ts. It need shadcn tool (npx shadcn@latest) create/update (question) /components/ui/custom-vars.ts if file not exits.

This is example for update component button

Add merge variant function / customVars

For example customVars.button is override button variants. I use chat GPT, so maybe _deepMerge fn work not correctly.

// /src/components/ui/custom-vars.ts
type GetObjDifferentKeys<
  T,
  U,
  T0 = Omit<T, keyof U> & Omit<U, keyof T>,
  T1 = {
    [K in keyof T0]: T0[K];
  },
> = T1;

type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>;

type MergeTwoObjects<
  T,
  U,
  T0 = GetObjDifferentKeys<T, U> & { [K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]> },
  T1 = { [K in keyof T0]: T0[K] },
> = T1;

export type DeepMergeTwoTypes<T, U> = [T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown }]
  ? MergeTwoObjects<NonNullable<T>, NonNullable<U>>
  : NonNullable<T> | NonNullable<U>;

function _deepMerge<T extends object, U extends object>(target: T, source: U): DeepMergeTwoTypes<T, U> {
  for (const key of Object.keys(source) as Array<keyof U>) {
    if (source[key] instanceof Object && key in target) {
      (target as any)[key] = _deepMerge((target as any)[key], source[key] as any);
    } else {
      (target as any)[key] = source[key];
    }
  }

  return target as any;
}

export function mergeVariants<T, U>(baseConfig: T, customConfig: U): DeepMergeTwoTypes<T, U> {
  return _deepMerge(baseConfig as any, customConfig as any) as any;
}

export const customVars = {
  button: {
    variants: {
      variant: {
        success: 'bg-success text-white hover:bg-success/80',
      },
    },
  },
};

Modify button.tsx

import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

// UPDATE: Import fn
import { customVars, mergeVariants } from './custom-vars';

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  // UPDATED: do mergeVariants
  mergeVariants(
    {
      variants: {
        variant: {
          default: 'bg-primary text-primary-foreground hover:bg-primary/90',
          destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
          outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
          secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
          ghost: 'hover:bg-accent hover:text-accent-foreground',
          link: 'text-primary underline-offset-4 hover:underline',
        },
        size: {
          default: 'h-10 px-4 py-2',
          sm: 'h-9 rounded-md px-3',
          lg: 'h-11 rounded-md px-8',
          icon: 'h-10 w-10',
        },
      },
      defaultVariants: {
        variant: 'default',
        size: 'default',
      },
    },
    customVars.button || {},
  ),
);

Usage button

Expected: Code should not throw error for variant='success'

<Button variant='success'>Success</Button>

Update CSS

@layer base {
  :root {
    /* Define new variables */   
    --success: 100 77% 44%;
    --success-foreground: 102 85% 34%;
  }
}

Update tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  theme: {
    extend: {
      colors: {
       
        success: {
          DEFAULT: 'hsl(var(--success))',
          foreground: 'hsl(var(--success-foreground))',
        },
      }
    }
  }
}

Affected component/components

Alert, Badge, Button, Label, Sheet, Toast, Toggle

Additional Context

Additional details here...

Before submitting

  • [X] I've made research efforts and searched the documentation
  • [X] I've searched for existing issues and PRs

doiya46 avatar Oct 18 '24 04:10 doiya46

i need this

victorhs98 avatar Oct 18 '24 22:10 victorhs98

You can just change the variants in the cva, you own the code after you install a component, thats the point of ShadcnUI.

imMatheus avatar Oct 18 '24 22:10 imMatheus

This issue has been automatically marked as stale due to two years of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you.

shadcn avatar Mar 02 '25 11:03 shadcn

No stale. Something went wrong with the stale bot.

shadcn avatar Mar 02 '25 11:03 shadcn

I can totally understand the request. I try to always extend the component somehow —this makes updates in the future much simpler.

So it would be nice to have an option to extend specific styles like the variants of the badge component.

Corepex avatar Mar 18 '25 18:03 Corepex

For merging variants, I was able to use extendTailwindMerge to safely merge custom utility classes with the cn utility: https://github.com/shadcn-ui/ui/discussions/6939


While I'm here, has anyone figured out a solution for overriding the defaultVariants for components? I'm thinking of setting up a function to set the defaultVariants for components using a single config to change common variants like borderRadius, size, color, etc. Seems like there might need to be a build step so the components are packaged with defaults specified for the app that consumes the components.

Although my example is for a monorepo use case, I think it might be useful for the component registry to have a way to update defaultVariants for components that use cva. I was thinking there could be a defaultVariants object inside of components.json to make this configurable in one place.

dvzrd avatar Apr 07 '25 17:04 dvzrd