Ticker Tape
Bandeau defilant de style marquee avec badges de prix et de variation. Installez Ticker Tape depuis le registre VLLNT UI avec la CLI shadcn.
Bandeau defilant de style marquee avec badges de prix et de variation. Fait partie de la famille data dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Ticker Tape a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/ticker-tape.jsonSource
import * as React from "react";
import { ArrowDownRight, ArrowUpRight, Dot } from "lucide-react";
import { formatChange } from "../../lib/format";
import { cn } from "../../lib/utils";
import { Badge } from "../badge/badge";
export type TickerTapeItem = {
change: number;
price: number | string;
symbol: string;
volume?: string;
};
export type TickerTapeProps = {
items: TickerTapeItem[];
pauseOnHover?: boolean;
speedSeconds?: number;
} & React.HTMLAttributes<HTMLDivElement>;
const tickerTapeKeyframes = `
@keyframes ticker-tape-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
`;
function formatPrice(price: number | string) {
return typeof price === "number" ? price.toLocaleString() : price;
}
function TickerTapeRow({ items }: { items: TickerTapeItem[] }) {
return (
<div className="flex min-w-max items-center gap-3 p-3">
{items.map((item) => {
const isPositive = item.change >= 0;
const TrendIcon = isPositive ? ArrowUpRight : ArrowDownRight;
return (
<div
className="flex min-w-[12rem] items-center gap-3 rounded-full border border-border/70 bg-background/80 px-3 py-2 shadow-sm"
key={`${item.symbol}-${item.price}-${item.change}`}
>
<div className="flex min-w-0 flex-col">
<span className="text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground">
{item.symbol}
</span>
<span className="truncate text-sm font-semibold text-foreground">
{formatPrice(item.price)}
</span>
</div>
<Badge
className={cn(
"ml-auto gap-1 rounded-full border px-2 py-0.5 text-[11px] tabular-nums",
isPositive
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400",
)}
variant="outline"
>
<TrendIcon className="size-3" />
{formatChange(item.change)}
</Badge>
{item.volume ? (
<span className="hidden items-center text-xs text-muted-foreground sm:inline-flex">
<Dot className="size-3.5" />
{item.volume}
</span>
) : null}
</div>
);
})}
</div>
);
}
export const TickerTape = ({
className,
items,
pauseOnHover = true,
ref: reference,
speedSeconds = 28,
...props
}: TickerTapeProps & { ref?: React.Ref<HTMLDivElement> }) => {
if (items.length === 0) {
return null;
}
return (
<div
aria-label="TickerTape"
className={cn(
"relative overflow-hidden rounded-2xl border border-border bg-card/70 backdrop-blur-sm",
className,
)}
ref={reference}
role="region"
{...props}
>
<style>{tickerTapeKeyframes}</style>
<div
className={cn(
"flex w-max items-stretch",
pauseOnHover && "hover:[animation-play-state:paused]",
)}
style={{
animationDuration: `${speedSeconds}s`,
animationIterationCount: "infinite",
animationName: "ticker-tape-scroll",
animationTimingFunction: "linear",
}}
>
<TickerTapeRow items={items} />
<div aria-hidden="true">
<TickerTapeRow items={items} />
</div>
</div>
</div>
);
};
TickerTape.displayName = "TickerTape";
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.