Write less. Ship more.
Drafting, summaries and translation inside the tools you already use. No credit card · 14-day trialHover the button
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.
This effect is free — the Buttons category contains 27 effects total, including 2 free. Effect.Labs has 811 vanilla effects. Explore the category →
Write less. Ship more.
Drafting, summaries and translation inside the tools you already use. No credit card · 14-day trialHover the button
Hover the Pro button
Hover the button
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.
#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.@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.: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.: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.#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.Requires unprefixed linear-gradient(), background-size and CSS transitions on background-position, transform and box-shadow — every browser since 2013. No JavaScript, zero dependencies.
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.
Copy the three blocks into your page. No dependencies.
<button class="demo-btn btn-gradient-shift">Gradient Shift</button>
.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;
}
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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. |
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+.
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.
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.