GODRICH

9 css button design ideas — candy, push, loading

css button design means deciding a button's colors, shape, and mouse reaction in a few lines of code.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine are not ranked by popularity; they follow the life of a button on a real screen. First the ones that catch the eye — candy gradient (01), shine sweep (02), gooey blob (03). Then the ones that answer a hover — border draw (04), icon slide (05), duotone wipe (06). Then the moment of the click — ripple ring (07), 3D push (08) — and finally the wait after the click, the pill loader (09).

01Candy gradient flow button

A candy-colored sheet three times wider than the button slides left by exactly one stripe period per loop. The only thing moving is a single translateX, so no layout is ever recalculated, and because the period is exactly 240px you never see the seam where the pattern repeats. It fits the sign-up button on a landing-page hero.

repeating-linear-gradienttranslateX(-240px) 한 주기overflow: hidden
.btn__bg {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 720px;
  background: repeating-linear-gradient(to right,
    $color 0 58px, $candy-b 64px 118px, $candy-c 124px 178px, $color 184px 240px);
}
.stage.is-demo .btn__bg { animation: gradient-flow $duration $easing infinite; }
@keyframes gradient-flow {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-240px); }
}

02Shine sweep button

A tilted white band crosses the dark button once, like light glancing across glass. The band is an ::after pseudo-element, laid at an angle with skewX(-20deg) and moved only with translateX, so it keeps working no matter what width the button becomes. Use it on checkout and buy buttons that deserve one more look.

translateX(±150%)skewX(-20deg)overflow: hidden
.btn::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: -60%;
  width: 60%;
  background: linear-gradient(115deg,
    transparent 0%, rgba(255, 255, 255, .85) 50%, transparent 100%);
  transform: skewX(-20deg) translateX(-10%);
}
@keyframes sweep {
  0%, 10% { transform: skewX(-20deg) translateX(-10%); }
  55%     { transform: skewX(-20deg) translateX(310%); }
  100%    { transform: skewX(-20deg) translateX(310%); }
}

03Gooey blob button

Two mint circles hidden behind the label swell left and right, so the button's silhouette stretches and shrinks like jelly. The circles share the button's exact color, so their edges merge into one outline, and swelling is a single scale. A natural fit for an add-to-cart button on a kids' shop.

::before·::after scaleborder-radius: 50%$ease-pop
.btn::before, .btn::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 56px; height: 56px;
  border-radius: 50%;
  background: $color;
  transform: translateY(-50%) scale(.3);
}
.stage.is-demo .btn::before { animation: blob-l $duration $easing infinite; }
@keyframes blob-l {
  0%, 12% { transform: translateY(-50%) scale(.3); }
  40%     { transform: translateY(-50%) scale(1.3) translateX(-8px); }
  100%    { transform: translateY(-50%) scale(.3); }
}

04Border-draw button

The button's outline draws itself in, stroke by stroke, as if by hand. Giving the SVG rectangle a pathLength="100" attribute turns any real length into a hundred-unit ruler, so dropping stroke-dashoffset from 100 to 0 is all it takes to draw the border. A pale wash fills in right after, and that completes one cycle.

stroke-dashoffsetpathLength=100vector-effect: non-scaling-stroke
.btn__rect {
  fill: none;
  stroke: $color;
  stroke-width: 3;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
}
.stage.is-demo .btn__rect { animation: draw $duration $easing infinite; }
@keyframes draw {
  0%, 12% { stroke-dashoffset: 100; }
  55%     { stroke-dashoffset: 0; }
  100%    { stroke-dashoffset: 100; }
}

05Icon slide button

Two cells sit side by side — one with the label, one with the label plus an arrow — and the whole track shifts by half its width. On hover the text slides out to the left while the arrow glides into its place from the right, and since the track is fixed at 200% width, no label length can break the alignment. Good for read-more links that promise a next step.

격층 translateXoverflow: hidden$ease-spring
.btn__track { display: flex; width: 200%; height: 100%; }
.btn__cell  { flex: 0 0 50%; display: flex; align-items: center; }
.stage.is-demo .btn__track { animation: slide-in $duration $easing infinite; }
@keyframes slide-in {
  0%, 18%  { transform: translateX(0); }
  55%, 72% { transform: translateX(-50%); }
  88%      { transform: translateX(-4%); }
  100%     { transform: translateX(0); }
}

06Duotone wipe button

The button splits into cream and peach, and the lower color unfolds from the left with scaleX, spreading across the whole face like paint. The label exists twice, stacked, and swaps with opacity, so it stays readable through the moment of the wipe. Suits the get-started button on pricing cards.

scaleX 레이어 와이프transform-origin: leftcolor 전환
.btn__wash {
  position: absolute;
  inset: 0;
  background: $color;
  transform: scaleX(0);
  transform-origin: left;
}
@keyframes wash {
  0%, 12%  { transform: scaleX(0); }
  48%, 70% { transform: scaleX(1); }
  100%     { transform: scaleX(0); }
}
.stage.is-demo .btn__t-on-dark { animation: swap-in $duration $easing infinite; }

07Ripple ring button

At the click, instead of a wave, an outlined ring spreads outward the way a bell's sound carries. A 3px ring sits in ::after and grows with scale while opacity erases it, so changing one color value drops it onto any icon button. The heart squashes and pops at the same instant, which makes it right for like and bookmark toggles.

::after 링 확산scale + opacity:active 트리거
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 3px solid $color;
  opacity: 0;
}
@keyframes ring {
  0%  { transform: scale(1);   opacity: .9; }
  34% { transform: scale(1.8); opacity: 0; }
  50% { transform: scale(1);   opacity: .9; }
  84% { transform: scale(1.8); opacity: 0; }
}

083D push-down button

A hard 8px shadow is laid under the button so it reads as a physical key, and pressing it pushes the face down by that same 8px with translateY while the shadow vanishes. The loop adds a small rebound when it releases; in real use :active needs just two lines. It belongs on game-start buttons and big tap targets in kids' apps.

translateY 눌림box-shadow 두께:active 트리거
$depth: 8px;
.btn {
  box-shadow: 0 $depth 0 darken($color, 22%),
              0 #{$depth + 6px} #{$sp-3} rgba(23, 20, 26, .22);
}
.btn:active { transform: translateY($depth); }
@keyframes press {
  0%, 20%  { transform: translateY(0); }
  35%, 55% { transform: translateY($depth); }
  70%      { transform: translateY(-3px); }
  85%,100% { transform: translateY(0); }
}

09Pill loader button

The moment you send a file, the label fades, the button deepens to a locked indigo, and inside its pill a short bar slides across twice. The bar moves only with translateX, and when the wait ends the label returns. In real use, toggling an is-busy class during your submit handler is all it takes.

라벨 opacity 스왑translateX 로더 막대$dur-loop 주기
.btn__bar {
  position: absolute;
  top: 0; bottom: 0;
  width: 60px;
  border-radius: $r-pill;
  background: $subject-cream;
  transform: translateX(-60px);
}
@keyframes bar-run {
  0%   { transform: translateX(-60px); }
  100% { transform: translateX(168px); }
}
@keyframes busy-bg {
  0%, 15%  { background: $color; }
  25%, 85% { background: $busy; }
  95%,100% { background: $color; }
}

Where it breaks — soft pastels are invisible to the machine

Item 01 was originally built the other way around: the background gradient itself was moved with background-position, and the palette was a softly blending pastel set. To the eye it clearly flowed, yet machine-measuring the rendered frames returned intensity 0.0 and zero moving frames. Between frames the gradient traveled 10px, and the color change across that distance stayed under the measurement threshold, so no pixel counted as changed. Redrawing it with crisp-edged stripes and moving the whole layer with translateX is what finally passed. The sweep distance in 02 is likewise set to several times the band width for the same reason. The archive password for the nine folders is 3vxxw8yh — type it exactly or the zip will not open. The gradient's stripe period (240px) must also match the travel distance exactly, which this item proved the hard way: off by as little as 4px and the pattern jumps once per cycle.

Accessibility

All nine fall back to animation: none under prefers-reduced-motion: reduce, and each one gets a still frame that still explains itself. Item 01 stops at a position where the stripes show, item 04 stops with the border fully drawn, and item 05 stops on the cell where label and arrow sit together. Item 09 parks the bar in the middle so color and shape still say "loading". Every button shows a :focus-visible outline for keyboard users. How to toggle the setting is documented in MDN's prefers-reduced-motion page.

FAQ

Can I use these without knowing JavaScript?

Yes — all nine run on CSS (SCSS) alone, without a single line of JavaScript. Even 09 only needs an is-busy class added and removed on the button in real use. If you want the basics like ripples and hovers, the 9 CSS button animations post covers them.

Where do I change the color to my brand color?

The $color variable at the top of each item's SCSS is the base. Item 01 adds two helper colors besides $color, and item 08 derives its dark edge automatically with darken($color, 22%). In item 06, $color is the under-layer peach, so change the button's background line too if you want to replace the top color.

What is inside the zip?

Each of the nine items ships its index.html, style.scss, and compiled style.css, plus a React-ready .tsx and .module.scss pair. Duration, easing, and color are exposed as props, so tweaking a value turns any of them into a variant. You can find more button material through site search.

Enter the archive password

The password is inside this article. You will find it as you read.