20 CSS Animated Buttons 19 / 20

CSS Next Prev Arrow Button Hover

Pagination and slider navigation controls where arrow icons slide subtly forward or backward on hover, with a functional slide counter driven by JS.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ab-19">
  <div class="ab-19__slides">
    <div class="ab-19__slide is-active" id="ab-19-slide">
      <p class="ab-19__slide-label">Slide 1 of 5</p>
      <h3 class="ab-19__slide-title">Effortless Deployment</h3>
      <p class="ab-19__slide-body">Push your code and watch it go live in seconds. Zero config, zero downtime.</p>
    </div>
  </div>
  <div class="ab-19__nav">
    <button class="ab-19__arrow ab-19__arrow--prev is-disabled" id="ab-19-prev" aria-label="Previous slide">
      <span class="ab-19__arrow-inner">
        <svg class="ab-19__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
        <svg class="ab-19__icon ab-19__icon--ghost" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
      </span>
    </button>
    <span class="ab-19__counter" id="ab-19-counter">01 / 05</span>
    <button class="ab-19__arrow ab-19__arrow--next" id="ab-19-next" aria-label="Next slide">
      <span class="ab-19__arrow-inner">
        <svg class="ab-19__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
        <svg class="ab-19__icon ab-19__icon--ghost" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
      </span>
    </button>
  </div>
</div>
.ab-19,.ab-19 *,.ab-19 *::before,.ab-19 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-19 ::selection{background:#6366f1;color:#fff}
.ab-19{
  font-family:system-ui,sans-serif;
  background:#0f172a;
  min-height:100vh;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:2rem;
  padding:2rem;
}
.ab-19__slides{
  width:100%;max-width:480px;
  background:#1e293b;
  border:1px solid rgba(255,255,255,.07);
  border-radius:16px;
  padding:2rem;
  min-height:130px;
}
.ab-19__slide-label{font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:#475569;margin-bottom:.6rem}
.ab-19__slide-title{font-size:1.3rem;font-weight:800;color:#f1f5f9;margin-bottom:.5rem}
.ab-19__slide-body{font-size:.88rem;color:#94a3b8;line-height:1.6}
.ab-19__nav{display:flex;align-items:center;gap:1.25rem}
.ab-19__counter{
  font-size:.85rem;
  font-weight:700;
  color:#94a3b8;
  letter-spacing:.08em;
  min-width:60px;
  text-align:center;
  transition:transform .2s;
}
.ab-19__counter.tick{animation:ab-19-countup .25s ease both}
@keyframes ab-19-countup{0%{transform:scale(1.3)}100%{transform:scale(1)}}
.ab-19__arrow{
  position:relative;
  width:44px;height:44px;
  display:flex;align-items:center;justify-content:center;
  background:#1e293b;
  border:1.5px solid rgba(255,255,255,.1);
  border-radius:10px;
  cursor:pointer;
  outline:none;
  color:#e2e8f0;
  transition:background .2s,border-color .2s,transform .15s;
  overflow:hidden;
}
.ab-19__arrow:hover:not(.is-disabled){background:#334155;border-color:rgba(99,102,241,.5);transform:scale(1.06)}
.ab-19__arrow.is-disabled{opacity:.35;cursor:not-allowed}
.ab-19__arrow-inner{
  display:flex;align-items:center;justify-content:center;
  position:relative;width:24px;height:24px;overflow:hidden;
}
.ab-19__icon{
  position:absolute;
  width:22px;height:22px;
  transition:transform .3s cubic-bezier(.4,0,.2,1);
}
.ab-19__icon--ghost{
  opacity:0;
  pointer-events:none;
}
/* next arrow hover */
.ab-19__arrow--next:hover:not(.is-disabled) .ab-19__icon:not(.ab-19__icon--ghost){transform:translateX(120%)}
.ab-19__arrow--next .ab-19__icon--ghost{transform:translateX(-120%);opacity:1}
.ab-19__arrow--next:hover:not(.is-disabled) .ab-19__icon--ghost{transform:translateX(0)}
/* prev arrow hover */
.ab-19__arrow--prev:hover:not(.is-disabled) .ab-19__icon:not(.ab-19__icon--ghost){transform:translateX(-120%)}
.ab-19__arrow--prev .ab-19__icon--ghost{transform:translateX(120%);opacity:1}
.ab-19__arrow--prev:hover:not(.is-disabled) .ab-19__icon--ghost{transform:translateX(0)}
@media(prefers-reduced-motion:reduce){
  .ab-19__icon,.ab-19__counter{transition:none}
  .ab-19__counter.tick{animation:none}
}
(function(){
  var prev=document.getElementById('ab-19-prev');
  var next=document.getElementById('ab-19-next');
  var counter=document.getElementById('ab-19-counter');
  var slide=document.getElementById('ab-19-slide');
  if(!prev||!next||!counter||!slide)return;
  var TOTAL=5;
  var idx=0;
  var TITLES=['Effortless Deployment','Instant Scaling','Built-in Observability','Zero Config Security','Global Edge Network'];
  var BODIES=[
    'Push your code and watch it go live in seconds. Zero config, zero downtime.',
    'Handle millions of requests without touching infrastructure settings.',
    'Real-time logs, traces, and metrics out of the box.',
    'Automatic TLS, DDoS protection, and secrets management included.',
    'Your app served from 300+ PoPs worldwide, under 20ms latency.'
  ];
  function pad(n){return n<10?'0'+n:String(n)}
  function update(){
    slide.querySelector('.ab-19__slide-label').textContent='Slide '+(idx+1)+' of '+TOTAL;
    slide.querySelector('.ab-19__slide-title').textContent=TITLES[idx];
    slide.querySelector('.ab-19__slide-body').textContent=BODIES[idx];
    counter.textContent=pad(idx+1)+' / '+pad(TOTAL);
    counter.classList.remove('tick');
    void counter.offsetWidth;
    counter.classList.add('tick');
    prev.classList.toggle('is-disabled',idx===0);
    next.classList.toggle('is-disabled',idx===TOTAL-1);
  }
  prev.addEventListener('click',function(){if(idx>0){idx--;update()}});
  next.addEventListener('click',function(){if(idx<TOTAL-1){idx++;update()}});
})();

How this works

Each nav button holds an SVG chevron arrow as its content. The arrow is wrapped in overflow: hidden so a cloned ghost arrow placed outside the visible area can slide in on hover. On :hover, a CSS transition slides the visible arrow out via translateX(100%) (for next) while the ghost copy slides in from translateX(-100%) using the sibling combinator — this creates the "arrow chasing arrow" effect. For the previous button the directions are inverted.

A connecting JS layer tracks the current slide index, updates the counter label between the two nav buttons, and applies .is-disabled classes at the boundary indices (first and last slide) to visually mute and functionally block the relevant button. The counter transition uses a @keyframes ab-19-countup that briefly scales the number on change for a tactile "tick" feel.

Customize

  • Change the arrow travel distance by editing the translateX value from 100% to 80% for a subtler slide or 130% for a more dramatic sweep.
  • Add a slide preview on hover by populating a data-preview attribute on each slide element and displaying it in a tooltip above the nav on next/prev hover.
  • Animate the counter vertically instead of with a scale pulse by replacing ab-19-countup with a translateY(-6px) → translateY(0) sequence for a slot-machine feel.
  • Connect to a real carousel by replacing the JS TOTAL constant with document.querySelectorAll(".slide").length and toggling an .is-active class on the current slide.
  • Use circular navigation (no disabled state at boundaries) by removing the boundary checks and using modulo arithmetic: idx = (idx + 1) % TOTAL.

Watch out for

  • The ghost arrow clone approach requires both the real and ghost elements to be present in the DOM at all times — avoid display: none for either; use opacity: 0; position: absolute to hide the ghost at rest.
  • The overflow: hidden on the button clips the arrow slide, but also clips any box-shadow that would otherwise show on hover — apply the shadow to a wrapper element instead.
  • On touch devices :hover persists after tap; add a touchstart event that immediately resolves the hover state to prevent the arrow from getting stuck mid-slide on mobile.

Browser support

ChromeSafariFirefoxEdge
49+ 10.1+ 44+ 49+

Uses CSS transitions and transforms universally supported in modern browsers.

Search CodeFronts

Loading…