Buttons✨ Premium

Slide Reveal

On hover, the label slides up by its own height in 400 ms and a second, green text written in the data-text attribute takes its place. One condition to check: the clip area must be the label's height — in the shipped code, both lines stay visible.

CSSTextReveal

Updated

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

3 usage examples

Download the app — download card — Slide Reveal example 1

① Download the app — download card

WhenApp card: icon, name, rating, a full-width button.
WhyHover reveals the platform and version — a useful detail at decision time, without cluttering the button at rest. The use case was on the previous page.
Settingsdata-text="macOS · v3.2", second label in #a5b4fc, left: 0; right: 0; text-align: center to center it.
Subscribe — newsletter field — Slide Reveal example 2

② Subscribe — newsletter field

WhenSign-up block: an email field, a submit button below.
WhyThe second text reassures (“0 spam”) right before commitment: the promise at the moment of doubt, slipped into the button rather than a footnote under the form. The previous page listed the form use case.
Settingsdata-text="0 spam", curve cubic-bezier(.2, .8, .2, 1), .3s.
Watch the trailer — game or film hero — Slide Reveal example 3

③ Watch the trailer — game or film hero

WhenDark hero: title, release date, a button that plays the video.
WhyHover reveals the duration (“▶ 2:14”): the user knows what they're committing to before clicking. Green on a dark fill stays highly legible even over a busy visual.
Settingsdata-text="▶ 2:14", padding: 0 40px with line-height: 56px, font-size: 17px.

How it works

The HTML carries both labels in a single element: <span class="btn-text" data-text="Let's Go!">Hover Me</span>. The first is the span's text; the second is generated by CSS: .btn-text::after { content: attr(data-text); position: absolute; left: 0; top: 100%; color: #22c55e } — it sits right below the first, one line-height down (18 px measured for 16 px text at line-height: normal), left-aligned. The span is display: inline-block; position: relative to serve as the reference.

On :hover, .btn-text gets transform: translateY(-100%) under transition: transform .4s cubic-bezier(.65, 0, .35, 1) — a symmetric ease-in-out curve (half the distance at half the time). The span rises by its height and carries its ::after along: the green text lands exactly where the white one was. Only transform is animated — composited layer, no layout recalculation. On exit, same trip in reverse.

The swap assumes that whatever leaves the frame is hidden. The code relies on overflow: hidden, declared twice — on .demo-btn and on .btn-slide-reveal. But a button's clip area is its padding box: 16 px above and below the text. The labels only move 18 px. Measured in Chromium on the shipped button (52 px tall): at rest, the green text sits 18 px below the white one, 16 of which are inside the padding — visible; on hover, the white one has moved up into the top padding — visible too. The shipped button reads as two lines, “Hover Me” above “Let's Go!”, at rest and on hover alike: the pair slides 18 px, nothing is revealed.

The clip must be the label's height. Two fixes. CSS only, on the button: padding-top: 0; padding-bottom: 0; line-height: 50px — the span inherits a 50 px line, top: 100% and translateY(-100%) become 50 px, the text stays centered and the button keeps its 52 px including the border. Structurally: a wrapping span display: inline-block; overflow: hidden; vertical-align: top around .btn-text, whose clip follows the text whatever the padding. The scenes on this page use the first; make sure one of the two ships with the code you paste.

Accessibility

  • Contrast: white on #1e1e2e = 16.4:1 and green #22c55e on #1e1e2e = 7.2:1 — AAA for both labels. The fill is opaque: the page color doesn't come into play. Shipped compliant.
  • The second label is read by screen readers: text generated through content: attr(data-text) is part of the accessible name in Chrome, Firefox and Safari — the button announces as “Hover Me Let's Go!”, both in a row. Give the button an explicit aria-label (the main label), or make the second text a restatement of the first rather than new information: what only appears on hover doesn't exist for keyboard or touch.
  • prefers-reduced-motion not handled: @media (prefers-reduced-motion: reduce) { .btn-slide-reveal .btn-text { transition: none } } — the swap becomes instant, without the slide.
  • Keyboard: no :hover on focus. .btn-slide-reveal:focus-visible .btn-text { transform: translateY(-100%) } reproduces the swap when focus arrives. The code doesn't remove outline: the native ring stays, outside the clipped area.
  • Touch: no hover; on iOS the second label stays shown after a tap (@media (hover: hover)). A label that changes on hover must leave the button identifiable by a stable name (WCAG 3.2.4): the aria-label guarantees it. Target 52 px tall, above the 44 px minimum.

Browser compatibility

Requires the CSS transition of transform, content: attr() on a pseudo-element and overflow: hidden — every browser since 2012. No JavaScript, zero dependencies.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Full (second label sticks after a tap)
Android Chrome✓ Full (on tap)

Without transitions, the two labels swap instantly on hover; the information is the same, the slide is missing.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<button class="demo-btn btn-slide-reveal">
  <span class="btn-text" data-text="Let's Go!">Hover Me</span>
</button>
Unlock the full code — $9.90/mo incl. tax

The full HTML + CSS + JS of Slide Reveal, ready to paste — plus all 840 effects. Or $299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
data-text (HTML — span attribute) Let's Go! The second label. Keep it close in length to the first: the ::after is absolute inside the span, its available width is the first text's — a longer text wraps (measured: three lines for “From $9 per month” under “Pricing”). white-space: nowrap prevents that, but it will then spill to the right.
color: #22c55e (CSS — ::after) #22c55e Color of the second label. inherit for the same white; an accent color to mark the state change.
transition: transform .4s cubic-bezier(.65, 0, .35, 1) (CSS — .btn-text) .4s, ease-in-out .25s: crisp; cubic-bezier(.34, 1.56, .64, 1): the second text overshoots and comes back — the first then briefly reappears from the top.
top: 100% (CSS — ::after) 100% (below) top: -100% with translateY(100%) on hover: the second text comes in from the top and the first drops down.
left: 0 (CSS — ::after) left-aligned left: 0; right: 0; text-align: center centers a shorter second label under the first; without it, it hugs the left.
Clip area (CSS — .btn-slide-reveal, fix) padding 16px 32px (two lines visible) padding: 0 32px; line-height: 50px: the clip takes the text's height and the swap works (see How it works). Or a wrapping span with overflow: hidden.
background: #1e1e2e + border rgba(255,255,255,.1) (CSS — .btn-slide-reveal) dark blue-gray Any opaque fill. On a light fill, revisit both text colors: the green #22c55e drops to 2.3:1 on white — #15803d gives 5.0:1.

FAQ

It should — but the shipped clip is the button's padding box, 16 px taller and lower than the text, while the labels only move 18 px: what “leaves” stays inside the padding, hence visible. Fix it with padding: 0 32px; line-height: 50px on .btn-slide-reveal (the clip then has the line's height), or wrap the span in a container with display: inline-block; overflow: hidden; vertical-align: top.
The ::after is absolutely positioned inside .btn-text: its available width is the first label's. If longer, it wraps its words. Set white-space: nowrap on the ::after and pick labels of similar length; if the second is shorter, center it with left: 0; right: 0; text-align: center.
Both texts, one after the other: content generated by content: attr(data-text) is folded into the button's accessible name by current browsers — “Hover Me Let's Go!”. Two remedies: aria-label="Hover Me" on the button, which pins the name whatever the state; or a second label that restates the first instead of adding information, so the double reading stays coherent.