20 CSS Animated Buttons19 / 20

CSS + JSMIT licensed

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.

Published

Live Demo
Try it

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()}});
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Animated Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Next Prev Arrow Button Hover
Source: https://codefronts.com/motion/css-animated-buttons/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.
## HTML
```html
<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>
```
## CSS
```css
.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;
  }
}
```

## JavaScript
```js
(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.

Make it yours

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

Gotchas — read before shipping

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

Techniques used in this demo

Search CodeFronts

Loading…