Cool Buttons
How to design and build a button for the pixel-perfect button grid that is genuinely cool — not a rectangle with a hover color. The bar: one strong idea per button, executed with material depth AND physical feedback. If the effect can't be described in one sentence ("a lens that refracts what's behind it", "a label that swaps with a blur"), the idea isn't sharp enough yet. If removing the hover state leaves a flat default button, it isn't done.
All paths below are relative to the repo root. Exemplar files are the source of truth — read the exemplar before building in its style; don't reconstruct the technique from memory.
Step 0 — Novelty check (always do this first)
Read
ButtonsArrinsrc/components/mine/grids/button-grid.tsx(names + descriptions are enough) and skim the filenames inregistry/new-york/buttons/.Confirm the new idea is not a re-skin of an existing button. Occupied territory:
Category Already built Materials neumorphic ( premium,orange-premium), metal/chrome (metal,blue-chrome,silver), glass (glass,glassy,soft-pill), matte (matte-dark,matte-shadow), bevel/inset (bevel,stripe)Motion label/icon swap ( visit,learn-more-buttion), toggles (toggle-buttion,blur-toggle,pearl-toggle), magnetic/cursor (mouse-follower), morph (morph,morph-image), shine sweep (shiny,liquid)Borders/glow gradient border ( border-gradient), rainbow glow (rainbow-glowing)Filters gooey ( goe), refraction lens (prism-glass)3D / press depth push ( 3d-button), stepper (recessed-stepper)If the request is vague ("make something cool"), pick from the idea bank below — choose the concept that's furthest from the occupied territory.
Idea bank (un-built as of 2026-07)
- Charge-to-confirm — hold to fill a ring/bar; release early and it drains back with elastic overshoot.
- Peel sticker — a corner lifts on hover (gradient + shadow fake curl), full peel on press.
- LED dot-matrix — label rendered as a dot grid; dots light up in a sweep, flicker on press.
(Head start:
src/lib/dotmatrix-core.tsx+dotmatrix-hooks.tsalready exist — reuse them.) - Slot machine / odometer label — characters roll vertically into place on hover.
- Shatter & self-repair — cracks into shards on press (clip-path pieces), reassembles with springs.
- Thermal ink — a heat trail follows the cursor across the surface and cools/fades behind it.
- Typewriter keycap — deep key travel, slight wobble, ink-stamp effect on the label at bottom of travel.
- Liquid fill level — a wavy liquid surface rises inside the button while held.
- Zipper reveal — a zipper pull drags across, opening to reveal the confirm state.
- Dual-action split — button splits into two halves (confirm/cancel) when pressed.
- Pixel-sort dissolve — label dissolves into sorted pixel streaks and reforms as new text.
- Spotlight caustics — a light cone follows the cursor with refracted caustic patterns inside.
- Paper fold — the button folds along a crease like paper on press, unfolds on release.
- Compass needle — an embedded needle/arrow tracks the cursor like a magnetic compass.
- Pressure gauge — an analog needle climbs while held, red-zone shake near the top.
- Conveyor label — the label rides in on a belt of repeating items and stops on the right one.
What makes a button cool — principles
- One idea. Pick a single effect and execute it fully. Stacking magnetic + gooey + gradient border reads as noise; each existing button in the grid is one clear trick.
- Material realism comes from layers. A convincing surface needs 3+ shadow layers:
ambient (large, soft, low opacity), key (tight, directional), and inset highlights
(top inner light edge + bottom inner dark edge). One
box-shadownever looks premium. - Physical press feedback is non-negotiable. Minimum
active:scale-95; better is depth compression (shadow layers shrink as the button "goes down" — see3d-button). - Motion has personality via easing. Springs with overshoot, elastic returns, blur
during transit. Linear or default
easereads as cheap. Durations: micro-feedback 100–200ms, transitions 300–500ms, idle loops slow (2s+) and subtle. - Letterpress the label on material buttons: a 1px
text-shadow(light below on dark surfaces, dark below on light) seats the text into the surface. - The effect must read within 1 second of hover. If it needs an explanation or a 10-second wait, it belongs in the playground, not the grid.
Technique recipes by tier
Rule: use the lowest tier that achieves the idea. CSS beats framer-motion beats GSAP beats SVG filters — each step up costs bundle size and complexity.
Tier 1 — CSS/Tailwind materials (no animation lib)
- Shadow-stack materials (neumorphic, matte, glossy): a
Record<Variant, {background, color, shadow}>map where the entire look is a 4–7 layerboxShadowstring in inlinestyle. Exemplars:registry/new-york/buttons/premium-button.tsx(canonical structure),metal-button.tsx(metallic gradients). - Gradient border (CSS can't gradient
border-color): outer element = gradient background +padding: <border-width>, inner element = actual fill. Exemplar:border-gradient-button.tsx. The user-level/gradient-borderskill has the full technique if needed. - 3D press physics: visible side/depth via stacked shadows; on
:active, translate down while shrinking the depth shadows by the same amount so the button "travels". Exemplar:3d-button.tsx. - Glass: translucent background +
backdrop-blur+ a light top inner-highlight + gradient ring. Exemplars:glass-button.tsx,soft-pill-button.tsx.
Tier 2 — framer-motion (framer-motion)
- Label/icon swap: two
<AnimatePresence mode="wait">blocks keyed on hover state; exit/enter withy-translate +filter: blur()+ slight rotate, ~100ms. Exemplar:visit-button.tsx. - Spring toggles / state changes:
motionlayout or animate props withtype: "spring"; tunestiffness/dampingfor overshoot. Exemplar:toggle-buttion.tsx(yes, filename has the typo — keep it). - Draggable buttons:
drag dragMomentum={false}onmotion.button. Exemplar:prism-glass-button.tsx. - Props extend
HTMLMotionProps<"button">(notButtonHTMLAttributes) so callers can pass motion props; forward the caller'sonHoverStart/handlers before your own logic.
Tier 3 — GSAP (gsap, @gsap/react)
- Magnetic / cursor-driven: map cursor position into a tween range with
gsap.utils.mapRange, parallax the label at a different strength, elastic return on leave, optionalCustomWiggleidle loop. - Shape morphs:
MorphSVGPluginbetween two paths insideuseGSAP. Exemplars:morph-button.tsx,morph-image-button.tsx(image-mask variant). - Register plugins at module scope (
gsap.registerPlugin(...)), run setup inuseGSAP/useEffect, and clean up: remove every listener andgsap.killTweensOf(target)in the cleanup function.
Tier 4 — SVG filters / canvas (the exotic tier)
- Gooey:
feGaussianBlur+feColorMatrixalpha-contrast trick on a group of blobs. Exemplar:goe-button.tsx. - Refraction / lens: generate a displacement map on a canvas (SDF math → data URL),
feed it through
feImage+feDisplacementMap— three displacement passes at slightly different scales for RGB chromatic split, thenfeBlend. Exemplar:prism-glass-button.tsx(also showsResizeObserver+useMemoregeneration when the button resizes). - Reach for this tier only when the idea is about the filter (goo, glass, distortion). Test in Chrome AND Safari — SVG filter support diverges.
Repo conventions (must follow)
- File:
registry/new-york/buttons/<kebab-name>.tsx."use client"at the top when it uses hooks/motion/GSAP; omit for purely presentational CSS buttons. - JSDoc one-liner at the very top (after
"use client") —registry-syncuses it as the public description:/** * A hold-to-confirm button that fills a progress ring and drains back if released early. */ - Component shape (dominant pattern — see
premium-button.tsx):React.forwardRef<HTMLButtonElement, Props>+.displayName+export default. Simple GSAP demos may use a plain function component. - Props:
extends React.ButtonHTMLAttributes<HTMLButtonElement>(CSS buttons) orHTMLMotionProps<"button">(motion buttons). Always acceptclassName(merge viacnfrom@/lib/utils— NOT@/lib/cn, which is legacy) andchildrenwith a sensible default label, and spread...props. - Variants:
export type XVariant = "a" | "b" | ...+ aRecord<XVariant, styles>map. The grid'sSelectwrappers import these exported types. - Division of labor: Tailwind for layout + interaction states (
px-*,rounded-*,active:scale-95,cursor-pointer), inlinestylefor rich visuals (multi-layer shadows, gradients). Don't fight Tailwind's arbitrary-value syntax for 6-layer shadows. - Self-contained: no playground scaffolding, no console logs, no dead code.
Grid + registry wiring
- Append to
ButtonsArrinsrc/components/mine/grids/button-grid.tsx, matching its exact shape — lowercasecomponent:holding a JSX element (this grid differs from the motion grid'sComponent:):{ name: "Charge Button", description: "Hold-to-confirm button that fills a ring and drains back on early release.", component: <ChargeButton>Hold me</ChargeButton>, registryName: "charge-button", },registryName=== the registry filename (kebab-case). Add the import at the top of the grid file. - If the button has variants or demo state, define a local
*Wrappercomponent above the array with the absolutely-positioned dashedSelectcontrol — copy the shape ofThreedButtonWrapperin the same file. - Pagination is automatic (
itemsPerPage) — just append. - Run
bun run registry-sync(never hand-editregistry.jsonorpublic/r/*.json), thenbun run lint. - No commit/push/PR from this skill. That happens only when Vansh explicitly says "run the flow" (see CLAUDE.local.md).
Quality checklist (before calling it done)
- Press feedback exists (
active:scale or depth travel). - Focus ring is visible for keyboard users (don't
outline-nonewithout a replacement). - Animates only
transform/opacity/filterwhere possible; nowidth/toptweens. - No layout shift on hover — the cell around it must not move.
- Looks right on the grid cell's background (test in the actual grid, not a white page).
- Listeners removed and tweens killed on unmount (GSAP), no leaked
ResizeObservers. - Renders standalone with zero props (default label + default variant).
-
disableddoesn't break the material (dim it, drop the hover motion).