All components
Marquee
An infinitely scrolling strip with faded edges that pauses for reduced motion.
StyleXReactAstroBunTypeScriptVite
123456789101112131415161718192021222324252627282930import { Marquee } from "#components/ui/base-ui/marquee.tsx";
const logos = ["StyleX", "React", "Astro", "Bun", "TypeScript"];
export function Example() { return ( // Content scrolls its children by -50%, so render the items TWICE (the // second copy aria-hidden) for a seamless loop; the edges fade the // strip and the animation pauses for prefers-reduced-motion. Every part // composes via the render prop. <Marquee.Root> <Marquee.Viewport> <Marquee.Content> {[0, 1].map((copy) => logos.map((name) => ( <Chip key={`${name}-${copy}`} aria-hidden={copy === 1 || undefined}> {name} </Chip> )), )} </Marquee.Content> </Marquee.Viewport> <Marquee.EdgeStart /> <Marquee.EdgeEnd /> </Marquee.Root> );}
// pace and fade are custom properties, overridable per instance:// <Marquee.Content style={styles.pace}> with { "--marquee-duration": "18s" }Install
1npx stylexblocks add marqueeSource
components/ui/base-ui/marquee.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'
const marqueeRoot = stylex.create({ base: { position: 'relative', width: '100%', '--marquee-edge-color': semanticColors.bg, '--marquee-edge-size': '20%', },})
const marqueeViewport = stylex.create({ base: { overflow: 'hidden', display: 'flex', width: '100%', height: '100%', },})
const marqueeContent = stylex.create({ base: { display: 'flex', minWidth: 'max-content', animationTimingFunction: 'linear', animationFillMode: 'forwards', animationDuration: 'var(--marquee-duration)', animationDelay: 'var(--marquee-delay)', animationIterationCount: 'var(--marquee-loop-count)', animationName: { default: null, '@media (prefers-reduced-motion: reduce)': 'none', }, },})
const marqueeEdge = stylex.create({ base: { position: 'absolute', zIndex: 1, pointerEvents: 'none', },})
// The slot recipe ships the timing custom properties (--marquee-duration,// --marquee-delay, --marquee-loop-count) but no keyframes, so the scroll// animation lives here with the component. Render the strip's items TWICE// inside Marquee.Content (the second copy aria-hidden) — the keyframes// translate by -50%, so the duplicate makes the loop seamless.const marqueeScroll = stylex.keyframes({ from: { transform: "translateX(0)" }, to: { transform: "translateX(-50%)" },});
const marqueeLocal = stylex.create({ content: { animationName: marqueeScroll, "--marquee-duration": "20s", "--marquee-delay": "0s", "--marquee-loop-count": "infinite", }, edgeStart: { backgroundImage: "linear-gradient(to right, var(--marquee-edge-color), transparent)", insetBlock: 0, insetInlineStart: 0, width: "var(--marquee-edge-size)", }, edgeEnd: { backgroundImage: "linear-gradient(to left, var(--marquee-edge-color), transparent)", insetBlock: 0, insetInlineEnd: 0, width: "var(--marquee-edge-size)", },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst marqueeSlotRecipe = defineSlotRecipe({ slots: { root: { styles: marqueeRoot }, viewport: { styles: marqueeViewport }, content: { styles: marqueeContent }, edge: { styles: marqueeEdge }, }, defaultVariants: {},});
/** * Marquee — recipe and component colocated in one file; each part composes * via `render`. Override the pace per instance through the custom * properties (`--marquee-duration`, `--marquee-delay`, `--marquee-loop-count`) * and the fade through `--marquee-edge-color` / `--marquee-edge-size`; the * recipe pauses the animation for prefers-reduced-motion. */const { withProvider, withContext } = createSlotRecipeContext(marqueeSlotRecipe);
export const Marquee = { Root: withProvider("div", "root", "MarqueeRoot"), Viewport: withContext("div", "viewport", "MarqueeViewport"), Content: withContext( "div", () => [marqueeContent.base, marqueeLocal.content], "MarqueeContent", ), EdgeStart: withContext( "div", () => [marqueeEdge.base, marqueeLocal.edgeStart], "MarqueeEdgeStart", ), EdgeEnd: withContext( "div", () => [marqueeEdge.base, marqueeLocal.edgeEnd], "MarqueeEdgeEnd", ),};