Buttons✨ Premium

Icon Slide

On hover, an arrow appears to the right of the label — opacity 0 → 1 and a 10 px slide in 300 ms — while the paddings swap (32/24 → 24/32 px): the button's width doesn't move, the text shifts 8 px. CSS only.

CSSHoverIcon

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

Continue — step of a multi-page form — Icon Slide example 1

① Continue — step of a multi-page form

WhenForm split into steps: a “2 of 4” indicator, two fields, a button to the next step.
WhyThe arrow appearing on hover says “there's more” right when the user hesitates, without cluttering the button at rest. The use case — a form's primary action — was already on the previous page.
SettingsText #451a03 (7.0:1), transition: padding .3s on the button, aria-hidden="true" on the arrow.
Start — lesson list of an online course — Icon Slide example 2

② Start — lesson list of an online course

WhenLesson list: title, duration, one small button per row.
WhyIn a list of identical actions, only the hovered row earns its arrow: the eye follows the cursor and the repetition doesn't weigh. The constant width keeps the buttons aligned across rows.
Settingspadding: 8px 14px 8px 18px (hover: 18px / 14px), gap: 6px, font-size: 13px, ↗ glyph for links that open a new page.
Proceed to payment — cart summary — Icon Slide example 3

③ Proceed to payment — cart summary

WhenOrder box: subtotal, shipping, total, a full-width button.
WhyAmber is a soft urgency color, the arrow announces the next step of a funnel — the button says what will happen. At full width the label's 8 px shift shows less, the arrow more.
Settingsjustify-content: center to center text and arrow, translateX(-16px) for a longer travel, .4s.

How it works

The .btn-icon-slide button is a flex container (display: flex; align-items: center; gap: 8px): the “Continue” text and the <span class="icon">→</span> are its two children, 8 px apart. Padding at rest: 16 px top and bottom, 32 px left, 24 px right (the padding-right: 24px overrides the 32 px from .demo-btn). Amber #f59e0b fill, white text.

The arrow is opacity: 0 and transform: translateX(-10px), but it stays in the flow: its 16 px and the 8 px gap are reserved at rest. Measured in Chromium on the shipped button (155.5 px): 32 px left of the text, 24 + 8 + 16 = 48 px to its right — the label sits 8 px left of the button's center, waiting for the invisible arrow.

On :hover, two things happen: .icon goes to opacity: 1; transform: translateX(0) under transition: .3s (all properties, ease curve), and the button swaps its paddings — padding-left: 24px; padding-right: 32px. The sum is unchanged (56 px), so is the width (155.5 px before and after): no shift of neighboring elements. But no transition is declared on .btn-icon-slide: the swap is instant. Measured 20 ms after hover: the label has already jumped 8 px left (x 224 → 216) while the arrow is at 1.5% opacity.

The arrow then travels its 10 px in 300 ms — relative to the text. On screen, since its own box moved 8 px left with the content, it ends only 2 px right of its invisible starting position (297.8 → 299.8 px): the perceived “slide” is mostly the label's jump plus the fade. One declaration changes the feel: transition: padding .3s on .btn-icon-slide, and the jump becomes a glide in sync with the arrow, the width staying constant at every instant since both paddings interpolate symmetrically. On exit, the arrow fades out sliding back 10 px in 300 ms and the paddings snap back. No :active, no shadow.

Accessibility

  • Severely insufficient contrast: white text on #f59e0b = 2.15:1 — the lowest of the catalog's buttons, under the 4.5:1 AA threshold and even under 3:1. Keep the amber and darken the text: #451a03 (7.0:1) or #1c1917 (8.1:1); if the text must stay white, darken the fill to #b45309 (5.0:1). The arrow inherits color: same fix, same gain.
  • The arrow is a real character (→, U+2192): screen readers announce “Continue, right arrow” — and opacity: 0 doesn't hide it from assistive technology. Put aria-hidden="true" on .icon (or replace it with an SVG carrying aria-hidden): the accessible name becomes “Continue” again.
  • prefers-reduced-motion not handled: @media (prefers-reduced-motion: reduce) { .btn-icon-slide .icon { transition: none } } — the arrow appears without sliding. Consider showing it permanently too (opacity: 1; transform: none): an arrow that only exists on hover is information (“there is a next step”) that touch and keyboard users never get.
  • Keyboard: no :hover on focus. Duplicate the rules with :focus-visible — .btn-icon-slide:focus-visible .icon { opacity: 1; transform: none } plus the padding swap — so Tab shows the arrow. Touch: no hover, so never an arrow on phones (except stuck after a tap on iOS). The native outline is kept.
  • Stability: the button's width is constant on hover (measured), nothing moves around it — valuable in a row of aligned buttons. Target 51 px tall (16 px padding + 19 px line), above the 44 px minimum.

Browser compatibility

Requires display: flex on a <button> and transitions on opacity/transform. Firefox only applied flex to a button's content from version 52 (2017): before that, text and arrow stay aligned but without the gap. No JavaScript, zero dependencies.

Chrome 29+✓ Full
Firefox 52+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Static (no hover; arrow stuck after a tap)
Android Chrome✓ Static (no hover)

Without flex, text and arrow stay inline without the 8 px gap; without transitions, the arrow appears at once on hover. In every case the button remains clickable and legible.

The code

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

index.html — structure
<button class="demo-btn btn-icon-slide">
  Continue
  <span class="icon">→</span>
</button>
Unlock the full code — $9.90/mo incl. tax

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

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
→ (HTML — content of .icon) → (U+2192) Any glyph or inline SVG: ↗, ↓ for a download, ✓ for a confirmation. An SVG with width: 1em; height: 1em; fill: currentColor inherits the text's size and color.
translateX(-10px) (CSS — .icon at rest) −10 px Distance the arrow travels. translateX(10px): it comes in from the right; translateY(6px): it rises.
transition: .3s (CSS — .icon) .3s ease Duration of the fade and slide. .2s: crisp; .45s cubic-bezier(.2, .8, .2, 1): damped landing. Add transition: padding .3s on .btn-icon-slide so the label glides at the same pace instead of jumping.
padding 32px / 24px → 24px / 32px (CSS — rest / hover, left / right) 32 / 24 → 24 / 32 The asymmetry makes the text's 8 px shift. Equal values (28px everywhere): no shift, only the arrow moves — and the width still stays constant.
gap: 8px (CSS — .btn-icon-slide) 8 px Text–arrow spacing, reserved even while the arrow is invisible. 6px tightens; 12px for a larger arrow.
background: #f59e0b (CSS — .btn-icon-slide) #f59e0b Shipped amber. For AA contrast: #451a03 text on this amber, or a #b45309 fill under the white text.
opacity: 0 (CSS — .icon at rest) 0 opacity: .35 hints at the arrow at rest: touch users see it, and the reserved space stops looking empty.

FAQ

Because the padding swap has no transition: only .icon has one. Add transition: padding .3s to .btn-icon-slide; both paddings interpolate at the same time and the width stays constant throughout the animation.
The invisible arrow still takes its place (16 px + 8 px gap) and the padding is asymmetric (32 / 24): the text sits 8 px left of center. Three options: own it (the button has a “direction”), show the arrow at opacity: .35, or take it out of the flow (position: absolute; right: 12px) — the width then no longer depends on the arrow, and the button must keep enough right padding to host it.
Put the SVG inside the span: <span class="icon" aria-hidden="true"><svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">…</svg></span>. The CSS rules target .icon and don't change; currentColor follows the text color, including once you fix it for contrast.