Vision UI

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

PropTypeDefault
variant?
"default" | "secondary" | "destructive" | "selected" | "link" | null
"default"
size?
"default" | "list" | "icon" | null
"default"
Edit on GitHub

Last updated on

On this page