AI Patterns

buttons

Shiny Button

A button with an animated light sweep across its surface.

"use client";

import * as React from "react";
import { motion, type HTMLMotionProps } from "motion/react";

import { cn } from "@/lib/utils";

export interface ShinyButtonProps extends Omit<HTMLMotionProps<"button">, "ref"> {
  children: React.ReactNode;
}

export function ShinyButton({ children, className, ...props }: ShinyButtonProps) {
  return (
    <motion.button
      whileTap={{ scale: 0.97 }}
      className={cn(
        "relative isolate overflow-hidden rounded-lg border border-white/10 bg-neutral-900 px-6 py-2.5 text-sm font-medium text-neutral-50 shadow-sm dark:bg-neutral-100 dark:text-neutral-900",
        className
      )}
      {...props}
    >
      <span className="relative z-10">{children}</span>
      <motion.span
        aria-hidden
        className="pointer-events-none absolute inset-0 z-0"
        style={{
          background:
            "linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.45) 50%, transparent 70%)",
          backgroundSize: "200% 100%",
        }}
        initial={{ backgroundPositionX: "150%" }}
        animate={{ backgroundPositionX: "-50%" }}
        transition={{
          repeat: Infinity,
          repeatType: "loop",
          duration: 2.2,
          ease: "linear",
          repeatDelay: 0.8,
        }}
      />
    </motion.button>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Shiny Button

## Summary
A primary call-to-action button with a looping light-sweep animation across its surface. The motion exists purely to draw the eye to the single most important action on the screen.

## When to use
- The one primary CTA on a screen (hero "Get Started", "Upgrade", "Try it now").
- Actions that begin the product's core flow.

## When not to use
- Secondary or tertiary actions — use a plain or outline button instead.
- More than one per view. The animation only works as a signal if it's rare; two shiny buttons on screen cancel each other out.
- Destructive actions (delete, cancel subscription). The celebratory motion sends the wrong emotional signal.
- A button that stays disabled or pending for a long time — stop the animation rather than looping over a dead action.

## Anatomy
- Label (text or short phrase).
- Looping sheen overlay (decorative, non-interactive).
- Press feedback (slight scale-down on tap/click).

## Behavior
- The sheen loops continuously and indefinitely while the button is enabled — it is not tied to a loading state.
- Press/tap gives immediate tactile feedback (subtle scale), independent of the sheen loop.
- On disabled state: stop the sheen animation and reduce opacity. A looping animation on a dead button reads as broken, not exciting.

## Content guidelines
- Short imperative verb phrase: "Get Started", "Try it", "Upgrade". No trailing punctuation.

## Accessibility
- Must render as a real `<button>` or link element, not a styled `<div>`.
- The sheen must never reduce the label's contrast below WCAG AA against the button background.
- The animation is decorative only — pause or remove it when the user has `prefers-reduced-motion` set.

## Related patterns
- None. This is a standalone action affordance, not part of an agent-status sequence.