Motion
Marquee
An endless CSS scroll of repeated content, horizontal or vertical, speed set with two CSS variables.
Guidelines#
Use it for logo strips, integration chips and testimonial columns (the vertical testimonial wall is three Marquees). Content must still make sense if the user never sees it all.
What you provide: children (one set of items, repeated for you), repeat (default 4), reverse (default false), vertical (default false), pauseOnHover (default false), className. Speed and spacing come from CSS variables set in className: --duration (default 40s) and --gap (default 1rem), for example [--duration:30s] [--gap:0.75rem].
Anatomy: a flex row (or column) with overflow-hidden, 8px padding and --gap between copies and between items. Add edge fades yourself (bg-gradient-to-r from-background overlays, as in this preview).
Behaviour: each copy runs marquee (translateX from 0 to calc(-100% - var(--gap))) or marquee-vertical, linear and infinite, over --duration; reverse flips the direction and pauseOnHover pauses the animation while hovered. There is no reduced-motion branch in the component; pause it ([animation-play-state:paused]) for users who ask for less motion.
Don't put links or buttons that must be clicked in a moving row, or set a duration so short the text cannot be read.
Facts#
| Group | Motion |
|---|---|
| Product | Crew OS |
| Global | window.DigitalCrew.Marquee |
| Source | digitalcrew-orchestrator: components/ui/marquee.tsx |
| Import in the app | import { Marquee } from "@/components/ui/marquee"; |
API#
TypeScript declarations emitted from components/ui/marquee.tsx.
interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
/**
* Optional CSS class name to apply custom styles
*/
className?: string;
/**
* Whether to reverse the animation direction
* @default false
*/
reverse?: boolean;
/**
* Whether to pause the animation on hover
* @default false
*/
pauseOnHover?: boolean;
/**
* Content to be displayed in the marquee
*/
children: React.ReactNode;
/**
* Whether to animate vertically instead of horizontally
* @default false
*/
vertical?: boolean;
/**
* Number of times to repeat the content
* @default 4
*/
repeat?: number;
}
export declare function Marquee({ className, reverse, pauseOnHover, children, vertical, repeat, ...props }: MarqueeProps): JSX.Element;Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var tools = [["Gmail", I.Mail], ["LinkedIn", I.Linkedin], ["Aircall", I.Phone], ["Google Calendar", I.Calendar], ["Google Meet", I.Video], ["HubSpot", I.Target]];
var quotes = [
["Booked 11 meetings in the first week.", "Omar Haddad, VP Finance"],
["Replies are triaged before I open my inbox.", "Claire Dubois, CFO"],
["Our SDRs finally work the warm leads only.", "Lena Fischer, Head of Sales"],
["Follow-ups never slip anymore.", "Marc Lefèvre, Founder"]
];
function App() {
return h("div", { className: "relative grid gap-1" },
h(DC.Marquee, { pauseOnHover: true, className: "[--duration:30s]" },
tools.map(function (t) { return h("span", { key: t[0], className: "inline-flex items-center gap-2 rounded-full border bg-card px-3 py-1.5 text-sm" }, h(t[1], { className: "size-4" }), t[0]); })),
h(DC.Marquee, { reverse: true, pauseOnHover: true, className: "[--duration:45s] [--gap:0.75rem]" },
quotes.map(function (q) { return h("span", { key: q[1], className: "inline-flex items-center gap-2 whitespace-nowrap rounded-lg bg-accent px-3 py-2 text-sm" }, "“" + q[0] + "”", h("span", { className: "text-muted-foreground text-xs" }, q[1])); })),
h("div", { className: "pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-background" }),
h("div", { className: "pointer-events-none absolute inset-y-0 right-0 w-16 bg-gradient-to-l from-background" }));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>