# Shine button

Pill button with a soft light sweep on hover, three variants, three sizes and an arrow that nudges forward.

- Type: component
- Category: Buttons
- Tier: Free
- Dependencies: lucide-react

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/shine-button.json
```

## Usage

```tsx
import { ShineButton } from "@/components/facade/shine-button";

export default function Page() {
  return <ShineButton />;
}
```

## Source

```tsx
import * as React from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";

export type ShineButtonVariant = "brand" | "solid" | "outline";
export type ShineButtonSize = "sm" | "md" | "lg";

export interface ShineButtonBaseProps {
  /** Visual style. Defaults to "brand". */
  variant?: ShineButtonVariant;
  /** Height and padding scale. Defaults to "md". */
  size?: ShineButtonSize;
  /** Optional leading icon. */
  icon?: React.ReactNode;
  /** Show a trailing arrow that nudges forward on hover. */
  arrow?: boolean;
  className?: string;
  children?: React.ReactNode;
}

type ShineButtonAsButton = ShineButtonBaseProps &
  Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ShineButtonBaseProps> & {
    href?: undefined;
  };

type ShineButtonAsAnchor = ShineButtonBaseProps &
  Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof ShineButtonBaseProps> & {
    href: string;
  };

/** Pass `href` to render an `<a>`, otherwise a `<button>` is rendered. */
export type ShineButtonProps = ShineButtonAsButton | ShineButtonAsAnchor;

const variantClasses: Record<ShineButtonVariant, string> = {
  brand: cn(
    "bg-brand text-brand-foreground",
    "shadow-[0_1px_2px_0_color-mix(in_oklch,var(--brand)_35%,transparent),0_10px_28px_-10px_color-mix(in_oklch,var(--brand)_70%,transparent)]",
    "hover:shadow-[0_1px_2px_0_color-mix(in_oklch,var(--brand)_35%,transparent),0_14px_34px_-10px_color-mix(in_oklch,var(--brand)_85%,transparent)]",
    "before:shadow-[inset_0_1px_0_0_color-mix(in_oklch,var(--brand-foreground)_28%,transparent)]",
  ),
  solid: cn(
    "bg-primary text-primary-foreground",
    "shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_18%,transparent),0_10px_24px_-12px_color-mix(in_oklch,var(--foreground)_45%,transparent)]",
    "before:shadow-[inset_0_1px_0_0_color-mix(in_oklch,var(--primary-foreground)_18%,transparent)]",
  ),
  outline: cn(
    "border border-border bg-background/70 text-foreground backdrop-blur",
    "shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_6%,transparent)]",
    "hover:border-foreground/20 hover:bg-muted/60",
  ),
};

const shineClasses: Record<ShineButtonVariant, string> = {
  brand: "via-brand-foreground/40",
  solid: "via-primary-foreground/25",
  outline: "via-brand/20",
};

const sizeClasses: Record<ShineButtonSize, string> = {
  sm: "h-8 gap-1.5 px-3.5 text-[13px] [&_svg]:size-3.5",
  md: "h-10 gap-2 px-5 text-sm [&_svg]:size-4",
  lg: "h-12 gap-2.5 px-6 text-[15px] [&_svg]:size-[18px]",
};

export function ShineButton(props: ShineButtonProps) {
  const { variant = "brand", size = "md", icon, arrow = false, className, children } = props;

  const classes = cn(
    "group relative isolate inline-flex shrink-0 select-none items-center justify-center overflow-hidden whitespace-nowrap rounded-full font-medium tracking-[-0.01em]",
    "transition-[box-shadow,background-color,border-color,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] active:scale-[0.98]",
    "before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit]",
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
    "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
    variantClasses[variant],
    sizeClasses[size],
    className,
  );

  const inner = (
    <>
      <span aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit]">
        <span
          className={cn(
            "absolute inset-y-0 -left-1/2 w-1/2 -skew-x-[20deg] bg-linear-to-r from-transparent to-transparent opacity-0",
            "transition-none group-hover:translate-x-[320%] group-hover:opacity-100 group-hover:transition-[transform,opacity] group-hover:duration-[900ms] group-hover:ease-[cubic-bezier(0.22,1,0.36,1)]",
            "group-focus-visible:translate-x-[320%] group-focus-visible:opacity-100 group-focus-visible:transition-[transform,opacity] group-focus-visible:duration-[900ms]",
            shineClasses[variant],
          )}
        />
      </span>
      {icon ? <span className="inline-flex shrink-0 items-center">{icon}</span> : null}
      {children ? <span className="relative">{children}</span> : null}
      {arrow ? (
        <ArrowRight
          aria-hidden
          className="shrink-0 transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1"
        />
      ) : null}
    </>
  );

  if (props.href !== undefined) {
    const {
      variant: _variant,
      size: _size,
      icon: _icon,
      arrow: _arrow,
      className: _className,
      children: _children,
      ...anchorProps
    } = props;
    return (
      <a {...anchorProps} className={classes}>
        {inner}
      </a>
    );
  }

  const {
    variant: _variant,
    size: _size,
    icon: _icon,
    arrow: _arrow,
    className: _className,
    children: _children,
    href: _href,
    type = "button",
    ...buttonProps
  } = props;
  return (
    <button {...buttonProps} type={type} className={classes}>
      {inner}
    </button>
  );
}

export default ShineButton;
```
