07 / 36
All 36 in 36 Tailwind CSS Button Hover Effects
- 01Tailwind Magnetic Ripple Attraction Button Hover Effect
- 02Tailwind Cyberpunk Holographic Glitch Grid Button
- 03Tailwind Frosted Glassmorphism Shimmer Glow Button
- 04Tailwind Organic Blob Morph Button Hover Effect
- 05Tailwind Dual-Line Intersection Border Draw Button
- 06Tailwind Isometric Tactile 3D Depress Button
- 07Tailwind Infinite Sliding Kinetic Marquee Button
- 08Tailwind Chroma Conic Gradient Border Spin Button
- 09Tailwind Minimalist Border & Outline Drawing Hover Effects
- 10Tailwind Modern 3D Skeuomorphic Button Press Effects
- 11Tailwind Neo-Brutalist & Cyberpunk Button Glow Effects
- 12Tailwind Sliding Background Color Wipe Button Effects
- 13Tailwind Liquid Fill & Organic Morphing Button Effects
- 14Tailwind Kinetic Text Flip & Icon Reveal Button Effects
- 15Tailwind Liquid Wave Fill Button Hover Effect
- 16Tailwind Magnetic Border Drift Button
- 17Tailwind Glitch Slice Button Hover Effect
- 18Tailwind Neon Pulse Glow Button Hover Effect
- 19Tailwind Split Reveal Curtain Button Effect
- 20Tailwind Diagonal Shutter Sweep Button Effect
- 21Tailwind Elastic Lift Spring Button Hover Effect
- 22Tailwind Center Underline Draw Link Button Effects
- 23Tailwind 3D Flip Card Button Hover Effect
- 24Tailwind Particle Burst Confetti Button Effect
- 25Tailwind Slide Doors Closing Panel Button Effect
- 26Tailwind Typewriter Retype Terminal Button Effect
- 27Tailwind Hard Shadow Shift Flat Button Effect
- 28Tailwind Material Ripple Wave Button Effect
- 29Tailwind Rainbow Gradient Border Spin Button
- 30Tailwind Morph Shape Silhouette Button Effects
- 31Tailwind Icon Slide In Directional Button Effect
- 32Tailwind Cursor Spotlight Glow Button
- 33Tailwind Stagger Fill Equaliser Button Effect
- 34Tailwind Sunburst Expand Radiant Button Effect
- 35Tailwind 3D Press Plunger Key Button Effect
- 36Tailwind Add-to-Cart Liquid Glass Ripple Button
Tailwind Infinite Sliding Kinetic Marquee Button
A kinetic editorial CTA that trades a static label for a continuously scrolling marquee on hover — the phrase repeats and slides endlessly across the pill. The port's real work is accessibility: the scrolling copies are aria-hidden decoration and the accessible name comes from aria-label, so what a screen reader hears never moves.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-07-group w-full min-h-svh flex flex-wrap">
<section class="tbh-07a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07a-stage" aria-label="Kinetic marquee button">
<button type="button" aria-label="Book tickets" class="group/btn relative overflow-hidden w-[230px] h-[60px] cursor-pointer border-0 rounded-full text-base font-extrabold tracking-[0.04em] text-white bg-tbh07a-acc focus-visible:outline-3 focus-visible:outline-tbh07a-acc focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 grid place-items-center transition-opacity duration-200 group-hover/btn:opacity-0 group-focus-visible/btn:opacity-0 motion-reduce:group-hover/btn:opacity-100 motion-reduce:group-focus-visible/btn:opacity-100">Book tickets</span>
<span aria-hidden="true" class="absolute inset-0 flex items-center gap-[34px] pl-[34px] whitespace-nowrap opacity-0 transition-opacity duration-200 group-hover/btn:opacity-100 group-hover/btn:animate-tbh07a-scroll group-focus-visible/btn:opacity-100 group-focus-visible/btn:animate-tbh07a-scroll motion-reduce:group-hover/btn:animate-none motion-reduce:group-hover/btn:opacity-0 motion-reduce:group-focus-visible/btn:animate-none motion-reduce:group-focus-visible/btn:opacity-0">
<span>Book tickets</span><span>Book tickets</span><span>Book tickets</span><span>Book tickets</span>
</span>
</button>
</section>
<section class="tbh-07b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07b-stage" aria-label="Vertical marquee button">
<button type="button" aria-label="Shop the drop" class="group/btn relative overflow-hidden w-[220px] h-[58px] cursor-pointer border-0 rounded-xl text-base font-extrabold text-white bg-tbh07b-acc focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 flex flex-col group-hover/btn:animate-tbh07b-roll group-focus-visible/btn:animate-tbh07b-roll motion-reduce:group-hover/btn:animate-none motion-reduce:group-focus-visible/btn:animate-none">
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
</span>
</button>
</section>
</div><div class="tbh-07-group w-full min-h-svh flex flex-wrap">
<section class="tbh-07a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07a-stage" aria-label="Kinetic marquee button">
<button type="button" aria-label="Book tickets" class="group/btn relative overflow-hidden w-[230px] h-[60px] cursor-pointer border-0 rounded-full text-base font-extrabold tracking-[0.04em] text-white bg-tbh07a-acc focus-visible:outline-3 focus-visible:outline-tbh07a-acc focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 grid place-items-center transition-opacity duration-200 group-hover/btn:opacity-0 group-focus-visible/btn:opacity-0 motion-reduce:group-hover/btn:opacity-100 motion-reduce:group-focus-visible/btn:opacity-100">Book tickets</span>
<span aria-hidden="true" class="absolute inset-0 flex items-center gap-[34px] pl-[34px] whitespace-nowrap opacity-0 transition-opacity duration-200 group-hover/btn:opacity-100 group-hover/btn:animate-tbh07a-scroll group-focus-visible/btn:opacity-100 group-focus-visible/btn:animate-tbh07a-scroll motion-reduce:group-hover/btn:animate-none motion-reduce:group-hover/btn:opacity-0 motion-reduce:group-focus-visible/btn:animate-none motion-reduce:group-focus-visible/btn:opacity-0">
<span>Book tickets</span><span>Book tickets</span><span>Book tickets</span><span>Book tickets</span>
</span>
</button>
</section>
<section class="tbh-07b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07b-stage" aria-label="Vertical marquee button">
<button type="button" aria-label="Shop the drop" class="group/btn relative overflow-hidden w-[220px] h-[58px] cursor-pointer border-0 rounded-xl text-base font-extrabold text-white bg-tbh07b-acc focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 flex flex-col group-hover/btn:animate-tbh07b-roll group-focus-visible/btn:animate-tbh07b-roll motion-reduce:group-hover/btn:animate-none motion-reduce:group-focus-visible/btn:animate-none">
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
</span>
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Four
.btn:hover .xselectors becomegroup-hover/btn:runs on the two layers, and the animation is applied where it is triggered — which matters here because the track must be animated ONLY while hovered, and that fact is now visible rather than inferred. - The keyframe distance (
-50%,-58px) still has to match the markup's copy count and row height. No variant can enforce that; it is the one coupling the port does not remove, and the gotchas say so. - UI change from the CSS original: design B now mirrors the roll on
focus-visible:and both designs' reduced-motion fallbacks return the button to its resting label instead of leaving a frozen mid-scroll track.
Here's a working CSS demo 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: Tailwind Infinite Sliding Kinetic Marquee Button
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-infinite-sliding-kinetic-marquee-button/
A kinetic editorial CTA that trades a static label for a continuously scrolling marquee on hover — the phrase repeats and slides endlessly across the pill. The port's real work is accessibility: the scrolling copies are aria-hidden decoration and the accessible name comes from aria-label, so what a screen reader hears never moves.
## HTML
```html
<div class="tbh-07-group w-full min-h-svh flex flex-wrap">
<section class="tbh-07a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07a-stage" aria-label="Kinetic marquee button">
<button type="button" aria-label="Book tickets" class="group/btn relative overflow-hidden w-[230px] h-[60px] cursor-pointer border-0 rounded-full text-base font-extrabold tracking-[0.04em] text-white bg-tbh07a-acc focus-visible:outline-3 focus-visible:outline-tbh07a-acc focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 grid place-items-center transition-opacity duration-200 group-hover/btn:opacity-0 group-focus-visible/btn:opacity-0 motion-reduce:group-hover/btn:opacity-100 motion-reduce:group-focus-visible/btn:opacity-100">Book tickets</span>
<span aria-hidden="true" class="absolute inset-0 flex items-center gap-[34px] pl-[34px] whitespace-nowrap opacity-0 transition-opacity duration-200 group-hover/btn:opacity-100 group-hover/btn:animate-tbh07a-scroll group-focus-visible/btn:opacity-100 group-focus-visible/btn:animate-tbh07a-scroll motion-reduce:group-hover/btn:animate-none motion-reduce:group-hover/btn:opacity-0 motion-reduce:group-focus-visible/btn:animate-none motion-reduce:group-focus-visible/btn:opacity-0">
<span>Book tickets</span><span>Book tickets</span><span>Book tickets</span><span>Book tickets</span>
</span>
</button>
</section>
<section class="tbh-07b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07b-stage" aria-label="Vertical marquee button">
<button type="button" aria-label="Shop the drop" class="group/btn relative overflow-hidden w-[220px] h-[58px] cursor-pointer border-0 rounded-xl text-base font-extrabold text-white bg-tbh07b-acc focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 flex flex-col group-hover/btn:animate-tbh07b-roll group-focus-visible/btn:animate-tbh07b-roll motion-reduce:group-hover/btn:animate-none motion-reduce:group-focus-visible/btn:animate-none">
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}
```Here's a working CSS demo 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: Tailwind Infinite Sliding Kinetic Marquee Button
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-infinite-sliding-kinetic-marquee-button/
A kinetic editorial CTA that trades a static label for a continuously scrolling marquee on hover — the phrase repeats and slides endlessly across the pill. The port's real work is accessibility: the scrolling copies are aria-hidden decoration and the accessible name comes from aria-label, so what a screen reader hears never moves.
## HTML
```html
<div class="tbh-07-group w-full min-h-svh flex flex-wrap">
<section class="tbh-07a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07a-stage" aria-label="Kinetic marquee button">
<button type="button" aria-label="Book tickets" class="group/btn relative overflow-hidden w-[230px] h-[60px] cursor-pointer border-0 rounded-full text-base font-extrabold tracking-[0.04em] text-white bg-tbh07a-acc focus-visible:outline-3 focus-visible:outline-tbh07a-acc focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 grid place-items-center transition-opacity duration-200 group-hover/btn:opacity-0 group-focus-visible/btn:opacity-0 motion-reduce:group-hover/btn:opacity-100 motion-reduce:group-focus-visible/btn:opacity-100">Book tickets</span>
<span aria-hidden="true" class="absolute inset-0 flex items-center gap-[34px] pl-[34px] whitespace-nowrap opacity-0 transition-opacity duration-200 group-hover/btn:opacity-100 group-hover/btn:animate-tbh07a-scroll group-focus-visible/btn:opacity-100 group-focus-visible/btn:animate-tbh07a-scroll motion-reduce:group-hover/btn:animate-none motion-reduce:group-hover/btn:opacity-0 motion-reduce:group-focus-visible/btn:animate-none motion-reduce:group-focus-visible/btn:opacity-0">
<span>Book tickets</span><span>Book tickets</span><span>Book tickets</span><span>Book tickets</span>
</span>
</button>
</section>
<section class="tbh-07b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh07-sans bg-tbh07b-stage" aria-label="Vertical marquee button">
<button type="button" aria-label="Shop the drop" class="group/btn relative overflow-hidden w-[220px] h-[58px] cursor-pointer border-0 rounded-xl text-base font-extrabold text-white bg-tbh07b-acc focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-[3px]">
<span aria-hidden="true" class="absolute inset-0 flex flex-col group-hover/btn:animate-tbh07b-roll group-focus-visible/btn:animate-tbh07b-roll motion-reduce:group-hover/btn:animate-none motion-reduce:group-focus-visible/btn:animate-none">
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
<span class="grid place-items-center h-[58px] flex-none">Shop the drop</span>
</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh07a-acc: #c8344a;
--color-tbh07a-acc: oklch(0.55 0.2 15);
--color-tbh07a-stage: #faf7f2;
--color-tbh07a-stage: oklch(0.97 0.008 80);
--color-tbh07b-acc: #6e5ad8;
--color-tbh07b-acc: oklch(0.55 0.18 285);
--color-tbh07b-stage: #0e0b1a;
--color-tbh07b-stage: oklch(0.15 0.03 285);
--font-tbh07-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-tbh07a-scroll: tbh07a-scroll 6s linear infinite;
--animate-tbh07b-roll: tbh07b-roll 1.6s linear infinite;
}
/* Stays CSS: the two loop keyframes. Both write the discrete translate property, so they
cannot collide with a translate-* utility added to the same track later. */
@keyframes tbh07a-scroll {
from {
translate: 0 0;
}
to {
translate: -50% 0;
}
}
@keyframes tbh07b-roll {
from {
translate: 0 0;
}
to {
translate: 0 -58px;
}
}
.tbh-07-group {
width: 100%;
min-height: 100svh;
}
.tbh-07a,
.tbh-07b {
min-height: 100svh;
}
```How this works
Seamlessness is arithmetic, not easing. The track holds four copies and the keyframe travels exactly -50%, so frame 1 and the final frame are pixel-identical and the loop has no seam. Halve the copies or change the distance and the jump becomes visible immediately.
Two layers, one cross-fade. The static label and the scrolling track both sit absolute inset-0; hover fades one out and the other in, then starts the animation. Because the track is only animated while hovered, an idle page of these buttons costs nothing.
Design B rolls vertically. Two stacked copies of the label in a column, translating by exactly one button height (-58px) — the same seam arithmetic on the other axis, and the pattern behind every 'Add to cart → Added' roll.
Make it yours
- Speed:
--animate-tbh07a-scroll's6s. Below ~3s the copy becomes unreadable. - Copy count: keep it even, and keep the keyframe at
-50%; those two facts are one decision. - Direction: negate the keyframe translate for a right-to-left run.
- Gap rhythm:
gap-[34px] pl-[34px]— the padding must equal the gap or the first repeat sits closer than the rest.
Gotchas — read before shipping
- The scrolling copies MUST be
aria-hiddenand the button MUST carryaria-label, or a screen reader announces the phrase four times. overflow-hiddenis load-bearing; without it the track escapes the pill and overlaps the page.- An odd number of copies cannot loop seamlessly at
-50%, whatever the duration. - Design B's column children need
flex-noneand an explicith-[58px], or flex shrinks them and the roll distance stops matching the keyframe.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Flexbox and keyframes are universal; with CSS unavailable the static label still renders as the button's text content.