Skip to content
← components

Candy Button

new

A glossy, puffed-up 3D call-to-action — drop shadow, blue edge ring, inset white stroke, and a top sheen. Lifts on hover, presses in on click.

palette
icon

hover to lift · press to sink · tab to focus

Usage

example.tsx
import { Plus } from "lucide-react";
import { CandyButton } from "~/components/buttons/candy-button";
// Recolor the whole button — gloss, ring, glow, focus — from one prop.
<CandyButton icon={<Plus />} color="#296FF0" radius={18} onClick={addMember}>
Add team member
</CandyButton>

How the puff works

There's no 3D here — just four stacked shadows. A bottom 0 3px 6px drop shadow lifts the button off the page. A 0 0 0 1px ring in the same blue seats the edge so it doesn't look cut out. An inset 1px white stroke is the bright rim, and an inset white blur near the top is the sheen that sells the gloss.

On hover it rises two pixels and the drop shadow grows and tints blue, so it appears to float. On :active it drops a pixel, scales down a hair, and swaps to an inset dark shadow — the light pushes in and it reads as physically pressed.

Tailwind

CandyButton.tsx
<button
className="
inline-flex items-center justify-center gap-2 rounded-[18px]
px-8 py-4 text-lg font-semibold text-white
bg-[linear-gradient(180deg,#3F7DF4,#296FF0)]
shadow-[0_3px_6px_rgba(0,0,0,0.2),0_0_0_1px_#296FF0,inset_0_0_0_1px_rgba(255,255,255,0.4),inset_0_1px_6px_2px_rgba(255,255,255,0.24)]
transition-[transform,box-shadow,filter] duration-150 ease-out
hover:-translate-y-0.5 hover:brightness-105
hover:shadow-[0_8px_18px_rgba(41,111,240,0.35),0_0_0_1px_#296FF0,inset_0_0_0_1px_rgba(255,255,255,0.45),inset_0_1px_6px_2px_rgba(255,255,255,0.3)]
active:translate-y-px active:scale-[0.99] active:brightness-95
focus-visible:outline-[3px] focus-visible:outline-offset-2 focus-visible:outline-[rgba(41,111,240,0.45)]
"
>
<Plus className="size-5" />
Add team member
</button>

Plain CSS (no Tailwind)

candy-button.css
.btn-candy {
--lift: 0px;
display: inline-flex;
align-items: center;
gap: 0.6rem;
border: none;
color: #fff;
border-radius: 18px;
padding: 0.95rem 1.9rem;
font: 600 1.125rem/1 system-ui, sans-serif;
background: linear-gradient(180deg, #3f7df4, #296ff0);
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.2), /* 1. drop — elevation */
0 0 0 1px #296ff0, /* 3. blue ring */
inset 0 0 0 1px rgba(255, 255, 255, 0.4), /* stroke: white 40% */
inset 0 1px 6px 2px rgba(255, 255, 255, 0.24); /* 2. top sheen */
transform: translateY(var(--lift));
transition:
transform 0.16s cubic-bezier(0.2, 0.7, 0.2, 1),
box-shadow 0.16s ease,
filter 0.16s ease;
}
.btn-candy:hover { --lift: -2px; filter: brightness(1.05); } /* rise */
.btn-candy:active { transform: translateY(1px) scale(0.99); /* sink */
box-shadow: 0 1px 3px rgba(0,0,0,.25), 0 0 0 1px #2563d8,
inset 0 2px 6px rgba(0,0,0,.2); }
.btn-candy:focus-visible { outline: 3px solid rgba(41,111,240,.45);
outline-offset: 2px; }

Props

proptypedefaultdescription
colorstring"#296FF0"Base color — gloss, ring, glow & focus all derive from it.
radiusnumber18Corner radius in px.
iconReactNodeOptional leading icon (e.g. lucide <Plus />).
labelColorstring"#fff"Label / icon color.
childrenReactNodeButton label.
disabledbooleanfalseDims and disables; cancels hover/press motion.
…propsButtonHTMLAttributesAny native button prop (onClick, type, aria-*).