1import { Button } from '@/components/ui/button';23export function Default() {4 return <Button>Click me</Button>;5}
Instalación
Copia y pega el siguiente codigo en tu proyecto.
'use client';import { Loading02Icon } from '@hugeicons/core-free-icons';import { HugeiconsIcon } from '@hugeicons/react';import { cva, type VariantProps } from 'class-variance-authority';import { AnimatePresence, HTMLMotionProps, motion, useReducedMotion } from 'motion/react';import React from 'react';import { cn } from '../lib/cn';// --- CVA ---const buttonVariants = cva('inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium outline-none select-none relative overflow-hidden disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed aria-invalid:ring-2 aria-invalid:ring-destructive/50 aria-invalid:border-destructive',{variants: {variant: {default:'bg-primary text-primary-foreground shadow-md hover:shadow-lg hover:shadow-primary/20',destructive:'bg-destructive text-destructive-foreground shadow-md hover:shadow-lg hover:shadow-destructive/20',outline: 'border-border border bg-background hover:bg-muted',secondary:'bg-secondary text-secondary-foreground shadow-md hover:shadow-lg hover:shadow-secondary/20',ghost: 'hover:bg-muted',link: 'text-primary underline-offset-4 hover:underline',},size: {default: 'h-10 px-4 py-2',sm: 'h-9 px-3 text-xs',lg: 'h-11 px-8 text-base',icon: 'size-9',},},defaultVariants: {variant: 'default',size: 'default',},},);// --- Animation variants (hoisted at module level) ---const LOADER_VARIANTS = {initial: { opacity: 0, scale: 0.8, filter: 'blur(4px)' },animate: { opacity: 1, scale: 1, filter: 'blur(0px)' },exit: { opacity: 0, scale: 0.8, filter: 'blur(4px)' },} as const;const CONTENT_VARIANTS = {initial: { opacity: 0, y: 5, filter: 'blur(4px)' },animate: { opacity: 1, y: 0, filter: 'blur(0px)' },exit: { opacity: 0, y: -5, filter: 'blur(4px)' },} as const;const SWAP_TRANSITION = { duration: 0.2 } as const;const BUTTON_SPRING = { type: 'spring', stiffness: 400, damping: 17 } as const;const BUTTON_STYLE = { willChange: 'transform' } as const;// --- Component ---interface ButtonProps extends HTMLMotionProps<'button'>, VariantProps<typeof buttonVariants> {loading?: boolean;loader?: React.ReactNode;leftIcon?: React.ReactNode;rightIcon?: React.ReactNode;fullWidth?: boolean;asChild?: boolean;children?: React.ReactNode;}const ButtonRoot = ({className,variant = 'default',size = 'default',loading = false,loader,leftIcon,rightIcon,fullWidth = false,disabled,children,type = 'button',ref,...props}: ButtonProps) => {const isDisabled = disabled || loading;const shouldReduceMotion = useReducedMotion();return (<motion.buttonref={ref}type={type}disabled={isDisabled}aria-busy={loading}className={cn(buttonVariants({ variant, size }), fullWidth && 'w-full', className)}whileTap={!isDisabled && !shouldReduceMotion ? { scale: 0.96 } : undefined}transition={BUTTON_SPRING}style={BUTTON_STYLE}{...props}><AnimatePresence mode="popLayout" initial={false}>{loading ? (<motion.spankey="loader"variants={LOADER_VARIANTS}initial="initial"animate="animate"exit="exit"transition={SWAP_TRANSITION}className="absolute flex items-center justify-center"aria-hidden="true">{loader ?? (<HugeiconsIcon icon={Loading02Icon} className="size-4 animate-spin" size={16} />)}</motion.span>) : (<motion.divkey="content"className="flex items-center gap-2"variants={CONTENT_VARIANTS}initial="initial"animate="animate"exit="exit"transition={SWAP_TRANSITION}>{leftIcon ? <span className="shrink-0">{leftIcon}</span> : null}<span>{children}</span>{rightIcon ? <span className="shrink-0">{rightIcon}</span> : null}</motion.div>)}</AnimatePresence></motion.button>);};ButtonRoot.displayName = 'Button';// --- ButtonGroup ---interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {children: React.ReactNode;orientation?: 'horizontal' | 'vertical';attached?: boolean;ref?: React.Ref<HTMLDivElement>;}const ButtonGroup = ({className,children,orientation = 'horizontal',attached = false,ref,...props}: ButtonGroupProps) => {return (<divref={ref}role="group"className={cn('inline-flex',orientation === 'horizontal' ? 'flex-row' : 'flex-col',attached? orientation === 'horizontal'? '[&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none': '[&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none': 'gap-2',className,)}{...props}>{children}</div>);};ButtonGroup.displayName = 'ButtonGroup';const Button = Object.assign(ButtonRoot, {Group: ButtonGroup,});export { Button, buttonVariants };export type { ButtonGroupProps, ButtonProps };
Asegurate de actualizar las rutas de importación según la estructura de tu proyecto.
Anatomía
import { Button } from '@/components/ui/button';
<Button variant="outline">Button</Button>
Ejemplos
Tamaños
1import { Plus, Settings } from 'lucide-react';2import { Button } from '@/components/ui/button';34export function Sizes() {5 return (6 <div className="flex flex-col gap-6">7 <div className="flex flex-wrap items-center gap-4">8 <Button size="sm">Small</Button>9 <Button size="default">Default</Button>10 <Button size="lg">Large</Button>11 <Button size="icon" aria-label="Settings">12 <Settings className="size-4" />13 </Button>14 </div>15 <div className="flex flex-wrap items-center gap-4">16 <Button size="sm" leftIcon={<Plus className="size-4" />}>17 Add small18 </Button>19 <Button size="default" leftIcon={<Plus className="size-4" />}>20 Add default21 </Button>22 <Button size="lg" leftIcon={<Plus className="size-5" />}>23 Add large24 </Button>25 </div>26 </div>27 );28}
Secondary
1import { Button } from '@/components/ui/button';23export function Secondary() {4 return <Button variant="secondary">Secondary</Button>;5}
Destructive
1import { Button } from '@/components/ui/button';23export function Destructive() {4 return <Button variant="destructive">Destructive</Button>;5}
Outline
1import { Button } from '@/components/ui/button';23export function Outline() {4 return <Button variant="outline">Outline</Button>;5}
Ghost
1import { Button } from '@/components/ui/button';23export function Ghost() {4 return <Button variant="ghost">Ghost</Button>;5}
Link
1import { Button } from '@/components/ui/button';23export function Link() {4 return <Button variant="link">Link</Button>;5}
Referencia de API
Button
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" | Estilo del botón. |
size | "default" | "sm" | "lg" | "icon" | "default" | Tamaño del botón. |
loading | boolean | false | Indica si el botón está en estado de carga. |
loader | React.ReactNode | <Loader2 /> | Componente loader personalizado para mostrar durante la carga. |
leftIcon | React.ReactNode | — | Ícono que aparece a la izquierda del contenido. |
rightIcon | React.ReactNode | — | Ícono que aparece a la derecha del contenido. |
fullWidth | boolean | false | Hace que el botón ocupe todo el ancho disponible. |
disabled | boolean | — | Desactiva el botón. |
className | string | — | Clases adicionales. |
type | "button" | "submit" | "reset" | "button" | Tipo de botón. |
children | React.ReactNode | — | Contenido del botón (texto, íconos, etc.). |
ButtonGroup
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Dirección del grupo de botones. |
attached | boolean | false | Une los botones (sin bordes redondeados entre ellos). |
className | string | — | Clases adicionales. |
children | React.ReactNode | — | Botones dentro del grupo. |