Button
Clickable elements
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
cn(
//* base *//
'relative flex min-h-[44px] min-w-[44px] items-center justify-center',
'text-[17px] leading-[22px] font-medium',
'rounded-md ring-offset-white *:pointer-events-none',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
"[font-feature-settings:'liga'_off,_'clig'_off]",
//* icon *//
"[&_[data-slot='icon']]:stroke-[2.25px]",
"[&_[data-slot='icon']]:transition-opacity [&_[data-slot='icon']]:duration-300",
"[&_[data-slot='icon']]:disabled:text-[color-mix(in_sRGB,white_10%,#5E5E5E_45%)]",
//* disabled *//
'disabled:pointer-events-none disabled:text-[color-mix(in_sRGB,white_10%,#5E5E5E_45%)]',
//* before *//
'before:[filter:blur(0.25px)] before:absolute before:inset-0 before:z-0 before:rounded-md',
'before:disabled:[linear-gradient(0deg,rgba(94,94,94,0.07)_0%,rgba(94,94,94,0.07)_100%),rgba(255,255,255,0.04)]',
'before:[transform:translateX(var(--btn-bg-translate-x))_translateY(var(--btn-bg-translate-y))]',
'before:[background-blend-mode:color-dodge,lighten] before:transition-opacity before:duration-300',
'before:[background:linear-gradient(0deg,rgba(94,94,94,0.24)_0%,rgba(94,94,94,0.24)_100%),rgba(255,255,255,0.12)]',
'before:opacity-0'
),
{
variants: {
/**
* @default "default"
*/
variant: {
default: cn(
'text-foreground/90',
//* icon *//
"[&_[data-slot='icon']]:text-foreground",
"[&_[data-slot='icon']]:opacity-[0.96]",
//* before *//
'before:opacity-75 before:hover:opacity-[0.96]'
),
secondary: cn(
'text-foreground/50 hover:text-foreground/90 transition-colors',
//* icon *//
"[&_[data-slot='icon']]:text-foreground",
"[&_[data-slot='icon']]:opacity-60",
"[&_[data-slot='icon']]:hover:opacity-[0.96]",
//* before *//
'before:hover:opacity-50 before:opacity-0',
'before:hover:[background:linear-gradient(0deg,rgba(94,94,94,0.24)_0%,rgba(94,94,94,0.24)_100%),rgba(255,255,255,0.12)]',
//* before[data-active]
'[&[data-active=true]]:before:opacity-40',
'[&[data-active=true]]:before:hover:[background:linear-gradient(0deg,rgba(94,94,94,0.24)_0%,rgba(94,94,94,0.24)_100%),rgba(255,255,255,0.12)]'
),
/* bg-destructive text-destructive-foreground */
destructive: cn(
'before:bg-destructive text-destructive-foreground/90',
//* icon *//
"[&_[data-slot='icon']]:text-destructive",
"[&_[data-slot='icon']]:opacity-70",
"[&_[data-slot='icon']]:hover:opacity-[0.96]",
//* before *//
'before:hover:opacity-50 before:opacity-0',
'before:hover:[background:linear-gradient(0deg,rgba(94,94,94,0.24)_0%,rgba(94,94,94,0.24)_100%),rgba(255,255,255,0.12)]'
),
selected: cn(
'text-background/[0.96]',
//* icon *//
"[&_[data-slot='icon']]:text-background [&_[data-slot='icon']]:z-[1]",
'before:[background:hsla(var(--foreground)/0.96)] before:text-background/90',
'before:hover:opacity-100 before:opacity-100'
),
link: cn(
'text-[#5ac8f5]',
//* icon *//
"[&_[data-slot='icon']]:text-[#5ac8f5]",
"[&_[data-slot='icon']]:opacity-70",
"[&_[data-slot='icon']]:hover:opacity-[0.96]",
//* before *//
'before:hover:opacity-50 before:opacity-0',
'before:hover:[background:linear-gradient(0deg,rgba(94,94,94,0.24)_0%,rgba(94,94,94,0.24)_100%),rgba(255,255,255,0.12)]'
),
},
/**
* @default "default"
*/
size: {
default: 'h-[2.75rem] px-[20px]',
list: 'h-[60px] px-[20px]',
icon: 'h-[2.75rem] w-[2.75rem]',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
type ButtonVariant = VariantProps<typeof buttonVariants>
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, ButtonVariant {
asChild?: boolean
}
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
function ButtonGroup({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'flex items-center justify-center gap-2 p-3',
'*:rounded-full [&_button:before]:rounded-full',
className
)}
{...props}
>
{children}
</div>
)
}
export { Button, buttonVariants, ButtonGroup }
export type { ButtonVariant, ButtonProps }
The Button Component is a custom implementation of a button using React and Framer Motion. It provides a dynamic button that responds to user interactions and changes appearance based on the elements it hovers over.
Examples
import { Ellipsis, PenBox, Share } from 'lucide-react'
import { Button, ButtonGroup } from '@/components/core/button'
import { Window } from '@/components/core/window'
export const ButtonExample = () => {
return (
<div className="flex flex-col items-center justify-center gap-4">
<ButtonGroup>
<Button>Action</Button>
<Button disabled>Action</Button>
<Button variant="secondary">Action</Button>
</ButtonGroup>
<Window thickness="thinner">
<ButtonGroup>
<Button variant="selected" className="rounded-full" size="icon">
<Share data-slot="icon" />
</Button>
<Button className="rounded-full" size="icon">
<Ellipsis data-slot="icon" />
</Button>
<Button className="rounded-full" size="icon" disabled>
<PenBox data-slot="icon" />
</Button>
</ButtonGroup>
</Window>
</div>
)
}
API
Prop | Type | Default |
---|---|---|
variant? | "default" | "secondary" | "destructive" | "selected" | "link" | null | "default" |
size? | "default" | "list" | "icon" | null | "default" |
Edit on GitHub
Last updated on