Text Marquee
Free · MITCSS-only scrolling text strip for announcements, joining phrases with a separator glyph and looping infinitely.
- Category
- Carousels & Tickers
- Framework
- astro
- Version
- 1.0.0
- Peer deps
- none
Install
velocityx add text-marquee
Free and MIT licensed — no account needed. The file lands under src/components/velocity/text-marquee/ by default (override with --dest), and it's yours to edit from the moment it's written.
Demo
View the live demo on the showcase site ↗
Source
Pulled live from the registry — this is the exact file velocityx add text-marquee writes into your project right now.
components/velocity/text-marquee/TextMarquee.astro
---
// VELOCITY COMPONENT · theming
//
// Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
// the Velocity preset. Out of the box they are a neutral indigo — NOT the
// Aidxn brand — so this drops into your project without dragging our look in.
//
// ⚡ NOT USING VELOCITY? Run this once, in your project root:
//
// velocityx theme
//
// It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
// tailwind.config, import the css, then set your whole brand in ONE place:
//
// :root {
// --vx-brand: custom;
// --vx-primary-600: 220 38 38; <- your action colour
// --vx-primary-700: 185 28 28;
// }
//
// ⚠️ Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
// breaks every /opacity variant in the library (bg-primary-600/20 etc).
//
// Prefer not to install anything? Find-and-replace `primary-` with your own
// scale name. Nothing else in this file depends on Velocity.
/**
* TextMarquee — infinite scrolling text/announcement strip.
*
* CSS-only, zero JS. Pass a list of phrases, they're joined with a
* separator glyph and duplicated for a seamless loop. Pauses on
* hover/focus, freezes under prefers-reduced-motion.
*
* Usage:
* <TextMarquee items={["Free shipping", "New drop every Friday"]} />
*/
export interface Props {
items: string[];
separator?: string;
speed?: number;
direction?: 'left' | 'right';
size?: 'sm' | 'md' | 'lg';
}
const { items, separator = '✦', speed = 22, direction = 'left', size = 'md' } = Astro.props;
const uid = `tm-${Math.random().toString(36).slice(2, 9)}`;
const sizeClass = {
sm: 'text-sm sm:text-base',
md: 'text-lg sm:text-2xl',
lg: 'text-2xl sm:text-4xl',
}[size];
---
<div
class="relative w-full overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_6%,black_94%,transparent)]"
role="region"
aria-label="Scrolling announcement"
>
<div
class={`${uid} flex w-max items-center gap-6 py-3 font-semibold uppercase tracking-tight text-secondary-900 dark:text-white whitespace-nowrap motion-reduce:animate-none ${sizeClass}`}
style={`--tm-duration:${speed}s; --tm-direction:${direction === 'left' ? 'normal' : 'reverse'};`}
>
{[0, 1].map(() => (
<div class="flex items-center gap-6 shrink-0">
{items.map((item, i) => (
<>
<span>{item}</span>
<span class="text-primary-500 dark:text-primary-400" aria-hidden="true">{separator}</span>
</>
))}
</div>
))}
</div>
</div>
<style is:global>
@keyframes v-ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
</style>
<style set:html={`
.${uid} {
animation: v-ticker-scroll var(--tm-duration) linear infinite;
animation-direction: var(--tm-direction);
}
.${uid}:hover, .${uid}:focus-within {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.${uid} {
animation: none !important;
flex-wrap: wrap;
width: 100%;
}
.${uid} > div:nth-child(2) {
display: none;
}
}
`}></style>
Theming
Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.