Skip to content
← components

Glossy Button

new

A tactile gradient button — stacked fill + hairline gradients with a three-part shadow for real depth. Dark and light variants, elegant hover/press states.

hover to lift · press to sink · tab to focus

Usage

example.tsx
import { GlossButton } from "~/components/buttons/gloss-button";
<GlossButton variant="dark" onClick={accept}>Accept</GlossButton>
<GlossButton variant="light" onClick={reject}>Reject</GlossButton>

How the depth works

The button stacks two gradients in one background: the fill is clipped to padding-box, and a lighter hairline gradient is clipped to border-box — so a 1px transparent border reveals a gradient stroke that catches light at the top, exactly like the Figma layers.

Depth is a three-part box-shadow: a soft 0 2px 4px ambient shadow lifts it off the page, a crisp 0 0 0 1px ring sharpens the edge, and an inset highlight adds a top sheen. Hover raises the lift and brightness; active sinks and dims it.

Tailwind

GlossButton.tsx
// The gradients need two background layers with different clips, so they
// live in `style`; Tailwind handles shape, shadow, and every state.
<button
style={{
background:
"linear-gradient(180deg,#201E25,#323137) padding-box," +
"linear-gradient(180deg,#4B4951,#313036) border-box",
}}
className="
inline-flex items-center justify-center rounded-[18px]
border border-transparent px-9 py-3.5 text-xl font-semibold text-zinc-100
shadow-[0_2px_4px_rgba(0,0,0,0.1),0_0_0_1px_#0D0D0D,inset_0_1px_0_rgba(255,255,255,0.06)]
transition-[transform,box-shadow,filter] duration-150 ease-out
hover:-translate-y-px hover:brightness-110
hover:shadow-[0_6px_16px_rgba(0,0,0,0.22),0_0_0_1px_#0D0D0D,inset_0_1px_0_rgba(255,255,255,0.09)]
active:translate-y-0 active:scale-[0.985] active:brightness-95
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500
"
>
Accept
</button>

Plain CSS (no Tailwind)

gloss-button.css
.btn-gloss {
--lift: 0px;
border: 1px solid transparent;
border-radius: 18px;
padding: 0.85rem 2.25rem;
font: 600 1.25rem/1 system-ui, sans-serif;
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;
}
/* Dark — "Accept" */
.btn-gloss--dark {
color: #f4f4f5;
background:
linear-gradient(180deg, #201e25, #323137) padding-box, /* fill */
linear-gradient(180deg, #4b4951, #313036) border-box; /* stroke */
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.1), /* ambient lift */
0 0 0 1px #0d0d0d, /* crisp ring */
inset 0 1px 0 rgba(255, 255, 255, 0.06); /* top sheen */
}
.btn-gloss--dark:hover { --lift: -1px; filter: brightness(1.08); }
.btn-gloss--dark:active { transform: scale(0.985); filter: brightness(0.96); }
/* Light — "Reject" */
.btn-gloss--light {
color: #1b1b1f;
background:
linear-gradient(rgba(227, 227, 227, 0.8), rgba(227, 227, 227, 0.8)) padding-box,
linear-gradient(180deg, #fdfdfd, rgba(241, 241, 241, 0)) border-box;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.7);
}

Props

proptypedefaultdescription
variant"dark" | "light""dark"Filled dark button, or the light secondary.
childrenReactNodeButton label.
disabledbooleanfalseDims and disables; cancels hover/press motion.
…propsButtonHTMLAttributesAny native button prop (onClick, type, aria-*).