UIPackage
Menu

Framework

Change language

Boilerplate repo

Button

button ui

Interactive button component with variants (default, destructive, outline, secondary, ghost, link), sizes, and composite ButtonGroup container for segmented toolbars and split buttons.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/button.json
Named registry: npx shadcn-vue@latest add @uipkge/button Installs to: app/components/ui/button/

Examples

Loading interactive previews…

Props

Name Type / Values Default Required
variant
'default''destructive''outline''secondary''ghost''link'
default optional
size
'default''sm''lg''xs''icon''icon-sm''icon-lg'
default optional

Used by

Files installed (4)

  • app/components/ui/button/Button.vue 1.7 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { Primitive } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import { buttonVariants } from './button.variants'
    
    // Why inline these unions instead of `ButtonVariants['variant']` /
    // `ButtonVariants['size']`:
    //
    // `ButtonVariants` is `VariantProps<typeof buttonVariants>`. cva's
    // type machinery wraps the variant keys in a conditional + indexed
    // access (`{ [K in keyof Variants]?: ... }`), which Vue 3.5+'s SFC
    // compiler can't resolve. Result: defineProps<> silently drops
    // `variant`/`size` from the runtime declarations, every <Button
    // variant="ghost"> falls back to `default`, and SidebarTrigger
    // renders as a filled primary block.
    //
    // Inline the unions and Vue extracts them cleanly. The cva runtime
    // still validates against the same option set at runtime; we just
    // give the SFC compiler a type it can handle.
    type Variant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
    type Size = 'default' | 'sm' | 'lg' | 'xs' | 'icon' | 'icon-sm' | 'icon-lg'
    
    interface Props {
      as?: string
      asChild?: boolean
      variant?: Variant
      size?: Size
      /** Native button type. Defaults to `button` so forms are not submitted accidentally. */
      type?: 'button' | 'submit' | 'reset'
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      as: 'button',
      type: 'button',
    })
    </script>
    
    <template>
      <Primitive
        data-uipkge
        data-slot="button"
        :data-variant="variant"
        :data-size="size"
        :as="as"
        :as-child="asChild"
        :type="as === 'button' && !asChild ? type : undefined"
        :class="cn(buttonVariants({ variant, size }), props.class)"
      >
        <slot />
      </Primitive>
    </template>
  • app/components/ui/button/ButtonGroup.vue 1.4 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    interface Props {
      class?: HTMLAttributes['class']
      orientation?: 'horizontal' | 'vertical'
      attached?: boolean
    }
    
    const props = withDefaults(defineProps<Props>(), {
      orientation: 'horizontal',
      attached: true,
    })
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="button-group"
        role="group"
        :data-orientation="orientation"
        :data-attached="attached ? '' : undefined"
        :class="
          cn(
            'inline-flex items-center',
            orientation === 'vertical' ? 'flex-col items-stretch' : 'flex-row',
            attached && [
              '[&>[data-slot=button]]:relative [&>[data-slot=button]:focus-visible]:z-20 [&>[data-slot=button]:hover]:z-10',
              orientation === 'horizontal' && [
                '[&>[data-slot=button]]:rounded-none',
                'first:[&>[data-slot=button]]:rounded-l-md last:[&>[data-slot=button]]:rounded-r-md',
                '[&>[data-slot=button]:not(:first-child)]:-ml-px',
              ],
              orientation === 'vertical' && [
                '[&>[data-slot=button]]:rounded-none',
                'first:[&>[data-slot=button]]:rounded-t-md last:[&>[data-slot=button]]:rounded-b-md',
                '[&>[data-slot=button]:not(:first-child)]:-mt-px',
              ],
            ],
            !attached && (orientation === 'vertical' ? 'gap-1' : 'gap-1.5'),
            props.class,
          )
        "
      >
        <slot />
      </div>
    </template>
  • app/components/ui/button/button.variants.ts 2.2 kB
    import type { VariantProps } from 'class-variance-authority'
    import { cva } from 'class-variance-authority'
    
    /**
     * Variant definitions live in their own file (rather than the package
     * `index.ts`) so `Button.vue` can `import { buttonVariants } from
     * './button.variants'` without creating a circular dependency through the
     * index. See card.variants.ts for the same pattern + the SSR symptom that
     * motivated the split.
     */
    export const buttonVariants = cva(
      "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-150 active:scale-[0.97] active:duration-100 touch-manipulation disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
      {
        variants: {
          variant: {
            default: 'bg-primary text-primary-foreground hover:bg-primary/90',
            destructive:
              'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
            outline:
              'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
            secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
            ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
            link: 'text-primary underline-offset-4 hover:underline',
          },
          size: {
            default: 'h-9 px-4 py-2 has-[>svg]:px-3',
            sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
            lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
            xs: 'h-7 rounded-md gap-1 px-2 has-[>svg]:px-1.5 text-xs',
            icon: 'size-9',
            'icon-sm': 'size-8',
            'icon-lg': 'size-10',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      },
    )
    
    export type ButtonVariants = VariantProps<typeof buttonVariants>
  • app/components/ui/button/index.ts 0.2 kB
    export { default as Button } from './Button.vue'
    export { default as ButtonGroup } from './ButtonGroup.vue'
    export { buttonVariants, type ButtonVariants } from './button.variants'

Raw manifest: https://uipkge.dev/r/vue/button.json