Buttons✨ Premium

Fill Slide Up

Light-purple outline button: on hover, a gradient pseudo-element rises from the bottom and fills the box in 600 ms on an ease-in-out curve while the text turns white — pure CSS, with a press that snaps without transition.

CSSHoverFill

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →

3 usage examples

See my work — secondary button of a portfolio — Fill Slide Up example 1

① See my work — secondary button of a portfolio

WhenHero of a portfolio or agency: a solid button for contact, an outline button for the work.
WhyThe outline marks the secondary action; the fill on hover gives it as much presence as the primary button the moment it's aimed at. The slow ease-in-out curve suits a site selling craft rather than speed.
Settings#7c3aed → #6d28d9 fill for contrast, transition: color .2s .15s on the text, padding: 16px 30px.
Read the article — blog card — Fill Slide Up example 2

② Read the article — blog card

WhenArticle list: image, category, title, excerpt and one read button per card.
WhyAn outline button doesn't weigh down a card grid; the fill rising on hover singles out the targeted card without changing its size. On a small button, the 44 px travel is still visible.
Settingspadding: 11px 18px, 13.5 px, border-radius: 8px (pseudo-element at 6px), .45s duration.
Send message — contact form — Fill Slide Up example 3

③ Send message — contact form

WhenShort form: name, email, message, a submit button aligned right.
WhyThe progressive fill says “ready to go” without aggression; it also works as the hover state of a disabled button that has just become active. The crisp press (scale(.95) with no transition) confirms submission.
SettingsAdd transform .15s to the transition to cushion the press, padding: 14px 28px, dark gradient for contrast.

How it works

At rest, .btn-fill-slide is hollow: background: transparent, border: 2px solid #a78bfa, color: #a78bfa. It carries position: relative and z-index: 1 — the stacking context that lets its ::after at z-index: -1 paint between the button's background and its text, instead of falling behind the page.

The fill is that ::after: position: absolute; bottom: 0; left: 0; width: 100%; height: 0, linear-gradient(135deg, #a78bfa, #8b5cf6) background, border-radius: 10px (the inner radius: the button's 12 px minus the 2 px border). On :hover, height goes to 100% — from 0 to 50 px on the demo button (54 px minus both borders) — in .6s cubic-bezier(.65, 0, .35, 1), a quartic ease-in-out curve: slow at the start, fast in the middle, braking at the end. overflow: hidden on .demo-btn clips the pseudo-element to the rounded corners.

The text follows the same curve and duration: transition: color .6s, from #a78bfa to #fff. Measured in Chromium at 300 ms: the fill reaches 57% of the height and the text is #dacefd — a very pale purple sitting on a purple background, 1.8:1 contrast. Since the letters occupy the middle third of the height, they are covered by the gradient between ≈ 230 and 400 ms while the text isn't white yet: the label is unreadable for about 150 ms on every hover.

On :active, transform: scale(.95). But the transition only covers color (and height for the pseudo-element): the squash is instant, no glide — measured at +20 ms, the scale is already 0.95. Release is just as abrupt. Add transform .15s to the list if you want a cushioned press.

Accessibility

  • Contrast at rest: #a78bfa on #0a0a0f = 7.3:1, AAA. On hover, insufficient contrast: white text on the #a78bfa → #8b5cf6 gradient = 2.7:1 and 4.2:1, below the 4.5:1 AA threshold. Darken the fill — linear-gradient(135deg, #7c3aed, #6d28d9) gives 5.7:1 and 7.1:1 — while keeping border and resting text at #a78bfa.
  • Readability during the transition: the text goes through 150 ms of unreadability (1.2 to 1.8:1) when the gradient covers it before it turns white. Give the text a shorter transition than the fill (transition: color .2s, or .25s with a .15s delay) so it is white when the wave reaches it.
  • Dark background required: on a light page, #a78bfa on white = 2.7:1 at rest. For a light theme, use #6d28d9 for the border and resting text.
  • prefers-reduced-motion not handled: the fill is a 50 px movement over 600 ms. Add @media (prefers-reduced-motion: reduce) { .btn-fill-slide, .btn-fill-slide::after { transition: none } } — the button snaps from outline to solid, white text included.
  • Keyboard and touch: no :hover on the keyboard — duplicate both hover rules under :focus-visible and :focus-visible::after; the native outline is preserved. On iOS the filled state sticks after a tap (@media (hover: hover)). Target ≈ 54 px tall, above the 44 px minimum.

Browser compatibility

Requires CSS transitions on color and height with cubic-bezier(), a positioned pseudo-element and a negative z-index — every browser since 2012. No JavaScript, zero dependencies.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Static (no hover, sticky state after a tap)
Android Chrome✓ Static (no hover)

Without transitions, the fill appears at once on hover, white text included: the button stays usable and readable, only the 600 ms rise is gone.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
transition-duration (CSS — .btn-fill-slide and ::after) .6s Duration of the fill and of the text color change. .35s = responsive; .9s = very slow, for a large isolated button only.
cubic-bezier(.65, 0, .35, 1) (CSS — transition) quartic ease-in-out Slow start and end, fast middle. cubic-bezier(.22, 1, .36, 1) (ease-out) starts the wave faster; linear gives a mechanical fill.
Direction (CSS — ::after: bottom / height) bottom to top Replace bottom: 0 with top: 0 to fill downward; for left to right, animate width (0 → 100%) with a fixed height: 100%.
background (CSS — ::after) linear-gradient(135deg, #a78bfa, #8b5cf6) Fill color. For AA contrast with white text: #7c3aed → #6d28d9. A solid color works too.
color (CSS — :hover) #fff Text color once filled. Dark text (#1e1b4b) on the original light purple gives 5.9:1 without changing the gradient.
border / color (CSS — .btn-fill-slide) 2px solid #a78bfa / #a78bfa Outline and text at rest. Match them to the gradient's light color so the wave seems to grow out of the border. On a light background, darken both.
transform (CSS — :active) scale(.95), no transition Press squash, instant. Add transform .15s to the button's transition list to cushion it, or remove the rule.

FAQ

The gradient covers the letters around 230–400 ms while the text, going from light purple to white over 600 ms, is still pale purple: 1.2 to 1.8:1 contrast. Three fixes: a faster text (transition: color .2s), a delayed-then-fast text (transition: color .25s .15s) so it turns white as the wave reaches it, or a darker fill (#7c3aed → #6d28d9), which contrasts with the intermediate pale purple as well as with the final white.
Left to right: keep left: 0; top: 0; height: 100% and animate width: 0 → 100%. From the center: left: 50%; width: 0 with translateX(-50%), then width: 100% on hover — or, lighter for rendering, transform: scaleX(0) → scaleX(1) with transform-origin: center, which triggers no layout recalculation.
The pseudo-element has z-index: -1. Without a stacking context of the button's own, it would paint behind the page background and the fill would be invisible. position: relative; z-index: 1 creates that context: the ::after paints above the button's transparent background but below its text. If you place the button inside a parent that already has isolation: isolate, the z-index: 1 is still needed.