{"slug":"shiny-button","category":"buttons","job":"start","added":"2026-09-11T19:54Z","title":"Shiny Button","description":"A button with an animated light sweep across its surface.","url":"https://ai-patterns.app/patterns/buttons/shiny-button","markdown":"https://ai-patterns.app/patterns/buttons/shiny-button.md","json":"https://ai-patterns.app/api/patterns/buttons/shiny-button","seenIn":[],"spec":"# Shiny Button\n\n## Summary\nA 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.\n\n## When to use\n- The one primary CTA on a screen (hero \"Get Started\", \"Upgrade\", \"Try it now\").\n- Actions that begin the product's core flow.\n\n## When not to use\n- Secondary or tertiary actions — use a plain or outline button instead.\n- 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.\n- Destructive actions (delete, cancel subscription). The celebratory motion sends the wrong emotional signal.\n- A button that stays disabled or pending for a long time — stop the animation rather than looping over a dead action.\n\n## Anatomy\n- Label (text or short phrase).\n- Looping sheen overlay (decorative, non-interactive).\n- Press feedback (slight scale-down on tap/click).\n\n## Behavior\n- The sheen loops continuously and indefinitely while the button is enabled — it is not tied to a loading state.\n- Press/tap gives immediate tactile feedback (subtle scale), independent of the sheen loop.\n- On disabled state: stop the sheen animation and reduce opacity. A looping animation on a dead button reads as broken, not exciting.\n\n## Content guidelines\n- Short imperative verb phrase: \"Get Started\", \"Try it\", \"Upgrade\". No trailing punctuation.\n\n## Accessibility\n- Must render as a real `<button>` or link element, not a styled `<div>`.\n- The sheen must never reduce the label's contrast below WCAG AA against the button background.\n- The animation is decorative only — pause or remove it when the user has `prefers-reduced-motion` set.\n\n## Related patterns\n- **Agent Setup Pill** — Use it instead when the call to action is connecting an agent, and don't show both on one screen.\n- **Stop Generation Button** — Use it instead when the button controls a running generation rather than inviting a first click.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { motion, type HTMLMotionProps } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ShinyButtonProps extends Omit<HTMLMotionProps<\"button\">, \"ref\"> {\n  children: React.ReactNode;\n}\n\nexport function ShinyButton({ children, className, ...props }: ShinyButtonProps) {\n  return (\n    <motion.button\n      whileTap={{ scale: 0.97 }}\n      className={cn(\n        \"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\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"relative z-10\">{children}</span>\n      <motion.span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 z-0\"\n        style={{\n          background:\n            \"linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.45) 50%, transparent 70%)\",\n          backgroundSize: \"200% 100%\",\n        }}\n        initial={{ backgroundPositionX: \"150%\" }}\n        animate={{ backgroundPositionX: \"-50%\" }}\n        transition={{\n          repeat: Infinity,\n          repeatType: \"loop\",\n          duration: 2.2,\n          ease: \"linear\",\n          repeatDelay: 0.8,\n        }}\n      />\n    </motion.button>\n  );\n}\n","dependencies":["motion"],"localImports":["@/lib/utils"]}