ButtonsFree

Gradient Shift

An indigo → violet → fuchsia gradient painted at twice the button's size: on hover the visible window slides from the indigo half to the fuchsia half in 500 ms, with a 3 px lift and an indigo glow — pure CSS, no @keyframes.

CSSHoverGradient
Hover or click the scene to interact

This effect is free — the Buttons category contains 27 effects total, including 2 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

New · AI assistant for teams

Write less. Ship more.

Drafting, summaries and translation inside the tools you already use. No credit card · 14-day trial

Hover the button

① Start for free — hero of a SaaS or AI tool

WhenHome page of a software product: headline, promise, a single trial button.
WhyIndigo → violet → fuchsia has been the SaaS and AI landing palette since 2023; the hover slide gives the primary button a relief that neighboring flat buttons lack, without adding anything to the HTML.
Settings17 px text, padding: 18px 40px, glow 0 18px 40px, stops darkened one step (#4f46e5, #7c3aed, #c026d3) for AA white text across the whole travel.
Starter 0 €/month
  • 1 project
  • Community support
  • Basic exports
Most popular Pro 19 €/month
  • Unlimited projects
  • Priority support
  • HD exports & API

Hover the Pro button

② Choose Pro — highlighted pricing card

WhenPricing grid: two or three cards, one of them recommended with a solid button.
WhyThe gradient marks the recommended plan without a loud badge, and the hover slide makes it “answer” the cursor — neighboring cards keep neutral buttons, the hierarchy reads in a second.
SettingsFull-width button, padding: 13px, 14.5 px, border-radius: 10px, lift reduced to translateY(-2px) and glow 0 10px 24px to stay inside the card.
Newsletter The Friday letter One email a week: three links worth your time, zero ads.
you@example.com
Unsubscribe in one click, anytime.

Hover the button

③ Subscribe — newsletter sign-up card

WhenShort block: a title, an email field and a submit button side by side.
WhyNext to a neutral field, the gradient button is the block's single colored element; the slide and the glow on hover confirm it is active without enlarging it, so the alignment with the field holds.
SettingsCompact button (padding: 12px 20px, 14 px), border-radius: 10px, .35s transition, glow 0 10px 24px for a tight block.

How it works

The base is the shared .demo-btn rule: padding: 16px 32px, 1 rem semi-bold text, border: none (shipped as its four longhands border-width/style/color/image), border-radius: 12px — 170 × 50 px measured in Chromium for the demo label. The gradient lives in the background shorthand of .btn-gradient-shift: linear-gradient(135deg, #6366f1, #8b5cf6, #d946ef) 0% 0% / 200% 200%, i.e. the gradient image (position 0% 0%, size 200% 200%) painted at twice the width and twice the height of the button — 340 × 100 px for the demo. Only the top-left quarter is visible at rest: the first half of the ramp, indigo → violet (measured #6865f2 in the top-left corner, #865df6 in the bottom-right corner).

On :hover, background-position: 100% 100% moves the visible window to the bottom-right quarter of the image: the second half of the ramp, violet → fuchsia (measured #9459f5#cf48ef). The gradient itself never changes — the window slides along the 135° diagonal. transition: 0.5s — shorthand for all .5s ease — animates that slide: at 250 ms the position is already at 80% and the lift at −2.4 px; the ease curve does most of the travel in the first half and settles gently.

Two more declarations ride the same transition: transform: translateY(-3px), which lifts the button without touching layout, and box-shadow: 0 15px 35px rgba(99, 102, 241, .4), a glow in the first stop's color, offset 15 px down and blurred over 35 px — it reaches about 50 px below the button (measured: #202148 6 px under the edge on the #0a0a0f background, back to the background around 45 px). When the cursor leaves, all three properties return along the same 500 ms curve.

Why the detour: a linear-gradient() is an image, and a transition cannot interpolate between two images — changing the color stops on hover would make the gradient snap. Painting the gradient larger and animating background-position, a plain length, is the standard technique to make it glide. No JavaScript, no @keyframes, no pseudo-element. position: relative and overflow: hidden, inherited from .demo-btn, are unused here (no child to clip) and do no harm.

Accessibility

  • Insufficient contrast as shipped: 16 px white text on #6366f1 = 4.47:1, on #8b5cf6 = 4.2:1, on #d946ef = 3.5:1 — below the 4.5:1 AA threshold across the whole travel, by a hair at rest (indigo → violet), clearly on hover (violet → fuchsia). Darken each stop one step: #4f46e5, #7c3aed, #c026d3 give 6.3:1, 5.7:1 and 4.7:1. An 18 px bold label (3:1 threshold) also passes with the original colors.
  • prefers-reduced-motion not handled: the slide, the lift and the glow animate regardless of the system preference. Add @media (prefers-reduced-motion: reduce) { .btn-gradient-shift { transition: none } } — both states remain, the change becomes instant. The motion is bounded anyway: 3 px and 500 ms, user-triggered, never looping.
  • Keyboard: there is no :hover on Tab navigation. Declare .btn-gradient-shift:focus-visible with the same three declarations as the hover so that focus slides and lifts like the mouse does. The code sets border: none without touching outline: the native focus ring stays and is not clipped by overflow: hidden.
  • Touch: on iOS the :hover state sticks after a tap (the button stays lifted, on its fuchsia half, until the next touch); wrap the hover rule in @media (hover: hover). Target ≈ 50 px tall, above the 44 px minimum.
  • Findability and screen readers: the button stands out on a dark background (#6366f1 on #0a0a0f = 4.4:1) as on a white one (3.5:1 at minimum, above the 3:1 threshold for a component's boundary). No generated content, no label change: the accessible name is the text, and the hover carries no information that color alone would have to convey.

Browser compatibility

Requires unprefixed linear-gradient(), background-size and CSS transitions on background-position, transform and box-shadow — every browser since 2013. No JavaScript, zero dependencies.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 7+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Full (:hover sticks after a tap)
Android Chrome✓ Full

Without transitions, both states apply instantly: the window jumps from the indigo half to the fuchsia half, the button stays usable. Without gradients (browsers before 2013), the whole background declaration is dropped and the button has no background at all — white text on the page: add background-color: #6366f1 right before the shorthand if you must serve those browsers.

The code

Copy the three blocks into your page. No dependencies.

HTML
<button class="demo-btn btn-gradient-shift">Gradient Shift</button>
CSS
.demo-btn  {
   padding: 16px 32px;
   font-size: 1rem;
   font-weight: 600;
   border-width: medium;
   border-style: none;
   border-color: currentcolor;
   border-image: none;
   border-radius: 12px;
   cursor: pointer;
   font-family: inherit;
   position: relative;
   overflow: hidden;
   }

.btn-gradient-shift  {
   background: linear-gradient(135deg, rgb(99, 102, 241), rgb(139, 92, 246), rgb(217, 70, 239)) 0% 0% / 200% 200%;
   color: white;
   transition: 0.5s;
   }

.btn-gradient-shift:hover  {
   background-position: 100% 100%;
   transform: translateY(-3px);
   box-shadow: rgba(99, 102, 241, 0.4) 0px 15px 35px;
   }
JavaScript (fx-0101)

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Color stops (CSS — linear-gradient(135deg, …)) #6366f1, #8b5cf6, #d946ef The 1st and 2nd stops make the rest state, the 2nd and 3rd the hover. For AA white text across the whole travel: #4f46e5, #7c3aed, #c026d3. Two stops are enough for a two-tone slide.
background-size (CSS — / 200% 200% in the shorthand) 200% 200% Hidden share of the gradient, hence the slide's travel. 150% = shorter trip, the two halves overlap; 300% = long trip, add a 4th stop so the colors don't stretch. At 100%, nothing moves anymore.
background-position on hover (CSS — :hover) 100% 100% Where the window lands. 100% 0% slides horizontally, 0% 100% vertically — match it to the gradient angle so the motion follows the colors.
transition (CSS — .btn-gradient-shift) 0.5s (all, ease) Duration of the slide, the lift and the glow — and of the return to rest. .3s = brisk; .8s = slow, almost liquid. Restrict to background-position, transform, box-shadow so a text-color change isn't animated with the same curve.
translateY (CSS — :hover) -3px Lift. 0 to keep only the color slide; -5px with a longer glow for a button that takes off.
box-shadow (CSS — :hover) 0 15px 35px rgba(99, 102, 241, .4) Indigo glow on hover, reaching 50 px below the button. Keep the 1st stop's color; shrink to 0 8px 20px in a tight card, and check that no overflow: hidden parent clips it.
Angle (CSS — 135deg) 135deg Direction of the ramp. Position 100% 100% follows the diagonal, so the slide appears to run “with” the colors. 90deg + position 100% 0% = horizontal left → right sweep.

FAQ

Why not animate the gradient colors directly on hover?

Because a linear-gradient() is an image: the browser cannot interpolate between two images, it flips from one to the other halfway through the transition. Two ways out: the one in the code — paint the gradient at 200% and slide background-position, an ordinary length, which has worked everywhere since 2013 — or declare each color as a custom property registered with @property (syntax: '<color>') and animate those variables, which truly changes the stops but requires Chrome 85+, Safari 16.4+ and Firefox 128+.

Can the gradient move continuously, without waiting for hover?

Yes, with an animation instead of the transition: @keyframes shift { to { background-position: 100% 100% } } and animation: shift 4s ease-in-out infinite alternate on .btn-gradient-shift. The 200% remains the travel; alternate avoids the jump on the way back. Keep the :hover rule for the lift and the glow, and stop the animation under prefers-reduced-motion: reduce — a permanently moving gradient is far more disruptive than a hover slide.

In my integration the gradient snaps instead of sliding.

Three causes, in order: a later rule redeclares background as a shorthand and resets background-size to auto — the window is then the size of the button and the position has nothing left to slide (check the computed value: 200% 200%); a * { transition: none } reset or a utility class overrides the transition; or the hover is simulated by a parent switching a class at once. If background-size and transition compute to 200% 200% and 0.5s, the slide works.