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.
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



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 inheritscolor: same fix, same gain. - The arrow is a real character (
→, U+2192): screen readers announce “Continue, right arrow” — andopacity: 0doesn't hide it from assistive technology. Putaria-hidden="true"on.icon(or replace it with an SVG carryingaria-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
:hoveron 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 nativeoutlineis 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.
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):
<button class="demo-btn btn-icon-slide">
Continue
<span class="icon">→</span>
</button>
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 / property | Default | Effect |
|---|---|---|
| → (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
.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.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.<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.