The arrow slides up and out of the circle while an identical arrow rises in from below, reading as motion toward the top. Two stacked icons in one grid cell, clipped by the button.
Also known as: scroll to top button css, arrow up icon hover, icon slide swap.
<button class="hfx-back-to-top-swap" aria-label="Back to top"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg> </button>
.hfx-back-to-top-swap {
--c: var(--hfx-color, #3b82f6);
font-size: var(--hfx-size, 16px);
position: relative;
display: grid;
place-items: center;
width: 3.5em;
height: 3.5em;
padding: 0;
border: 0;
border-radius: 50%;
cursor: pointer;
overflow: hidden;
background: color-mix(in srgb, var(--c) 12%, transparent);
color: var(--c);
transition: background-color 0.3s ease, color 0.3s ease;}
.hfx-back-to-top-swap svg {
grid-area: 1 / 1;
width: 1.5em;
height: 1.5em;
transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.hfx-back-to-top-swap svg:nth-of-type(2) { transform: translateY(2.6em); }
.hfx-back-to-top-swap:is(:hover, :focus-visible) {
background: var(--c);
color: #fff;
}
.hfx-back-to-top-swap:is(:hover, :focus-visible) svg:nth-of-type(1) { transform: translateY(-2.6em); }
.hfx-back-to-top-swap:is(:hover, :focus-visible) svg:nth-of-type(2) {
transform: translateY(0);
transition-delay: 0.05s;
}
@media (prefers-reduced-motion: reduce) {
.hfx-back-to-top-swap, .hfx-back-to-top-swap *, .hfx-back-to-top-swap::before, .hfx-back-to-top-swap::after, .hfx-back-to-top-swap *::before, .hfx-back-to-top-swap *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}The same effect using Tailwind utility classes. No custom CSS or config needed.
<button aria-label="Back to top" class="group grid h-12 w-12 place-items-center overflow-hidden rounded-full bg-blue-500/10 text-blue-500 transition-colors duration-300 hover:bg-blue-500 hover:text-white"> <svg class="h-6 w-6 transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] [grid-area:1/1] group-hover:-translate-y-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg> <svg class="h-6 w-6 translate-y-10 transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] [grid-area:1/1] group-hover:translate-y-0 group-hover:delay-75" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg> </button>
--hfx-color on the element or a parent.
Currently previewing #3b82f6.--hfx-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.