Dice UI
Components

Marquee

An animated scrolling component that continuously moves content horizontally or vertically.

API
"use client";
 
import {
  Marquee,
  MarqueeContent,
  MarqueeEdge,
  MarqueeItem,
} from "@/components/ui/marquee";
 
const tricks = [
  {
    title: "Kickflip",
    description:
      "A kickflip is a trick where you kick the board forward while jumping, and then land on the board with the other foot.",
  },
  {
    title: "Heelflip",
    description:
      "A heelflip is a trick where you flip the board backwards while jumping, and then land on the board with the other foot.",
  },
  {
    title: "Tre Flip",
    description:
      "A tre flip is a trick where you flip the board sideways while jumping, and then land on the board with the other foot.",
  },
  {
    title: "FS 540",
    description:
      "A fs 540 is a trick where you flip the board 540 degrees while jumping, and then land on the board with the other foot.",
  },
  {
    title: "360 Varial McTwist",
    description:
      "A 360 varial mc twist is a trick where you flip the board 360 degrees while jumping, and then land on the board with the other foot.",
  },
];
 
export function MarqueeDemo() {
  return (
    <Marquee
      aria-label="Skateboard tricks showcase"
      pauseOnHover
      pauseOnKeyboard
    >
      <MarqueeContent>
        {tricks.map((trick) => (
          <MarqueeItem key={trick.title} asChild>
            <div className="flex w-[260px] flex-col gap-1 rounded-md border bg-card p-4 text-card-foreground shadow-sm">
              <div className="font-medium text-sm leading-tight sm:text-base">
                {trick.title}
              </div>
              <span className="line-clamp-2 text-muted-foreground text-sm">
                {trick.description}
              </span>
            </div>
          </MarqueeItem>
        ))}
      </MarqueeContent>
      <MarqueeEdge side="left" />
      <MarqueeEdge side="right" />
    </Marquee>
  );
}

Installation

CLI

npx shadcn@latest add "https://diceui.com/r/marquee"

Manual

Install the following dependencies:

npm install @radix-ui/react-slot class-variance-authority

Copy and paste the following code into your project.

"use client";
 
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { useComposedRefs } from "@/lib/compose-refs";
import { cn } from "@/lib/utils";
 
const ROOT_NAME = "Marquee";
const CONTENT_NAME = "MarqueeContent";
 
type Side = "left" | "right" | "top" | "bottom";
type Orientation = "horizontal" | "vertical";
type Direction = "ltr" | "rtl";
 
type RootElement = React.ComponentRef<typeof MarqueeRoot>;
type ContentElement = React.ComponentRef<typeof MarqueeContent>;
 
interface Dimensions {
  width: number;
  height: number;
}
 
interface ElementDimensions {
  rootSize: number;
  contentSize: number;
}
 
function createResizeObserverStore() {
  const listeners = new Set<() => void>();
  let observer: ResizeObserver | null = null;
  const elements = new Map<Element, Dimensions>();
  const refCounts = new Map<Element, number>();
  const isSupported = typeof ResizeObserver !== "undefined";
  let notificationScheduled = false;
 
  const snapshotCache = new WeakMap<
    Element,
    WeakMap<
      Element,
      { horizontal: ElementDimensions; vertical: ElementDimensions }
    >
  >();
 
  function notify() {
    if (notificationScheduled) return;
    notificationScheduled = true;
    queueMicrotask(() => {
      notificationScheduled = false;
      for (const callback of listeners) {
        callback();
      }
    });
  }
 
  function cleanup() {
    if (observer) {
      observer.disconnect();
      observer = null;
    }
    elements.clear();
    refCounts.clear();
  }
 
  function subscribe(callback: () => void) {
    listeners.add(callback);
    return () => {
      listeners.delete(callback);
      if (listeners.size === 0) {
        cleanup();
      }
    };
  }
 
  function getSnapshot(
    rootElement: RootElement | null,
    contentElement: ContentElement | null,
    orientation: Orientation,
  ): ElementDimensions | null {
    if (!rootElement || !contentElement) return null;
 
    const rootDims = elements.get(rootElement);
    const contentDims = elements.get(contentElement);
 
    if (!rootDims || !contentDims) return null;
 
    const rootSize =
      orientation === "vertical" ? rootDims.height : rootDims.width;
    const contentSize =
      orientation === "vertical" ? contentDims.height : contentDims.width;
 
    let rootCache = snapshotCache.get(rootElement);
    if (!rootCache) {
      rootCache = new WeakMap();
      snapshotCache.set(rootElement, rootCache);
    }
 
    let contentCache = rootCache.get(contentElement);
    if (!contentCache) {
      contentCache = {
        horizontal: { rootSize: -1, contentSize: -1 },
        vertical: { rootSize: -1, contentSize: -1 },
      };
      rootCache.set(contentElement, contentCache);
    }
 
    const cached = contentCache[orientation];
    if (cached.rootSize === rootSize && cached.contentSize === contentSize) {
      return cached;
    }
 
    const snapshot = { rootSize, contentSize };
    contentCache[orientation] = snapshot;
    return snapshot;
  }
 
  function observe(
    rootElement: RootElement | null,
    contentElement: Element | null,
  ) {
    if (!isSupported || !rootElement || !contentElement) return;
 
    if (!observer) {
      observer = new ResizeObserver((entries) => {
        let hasChanged = false;
 
        for (const entry of entries) {
          const element = entry.target;
          const { width, height } = entry.contentRect;
 
          const currentData = elements.get(element);
 
          if (
            !currentData ||
            currentData.width !== width ||
            currentData.height !== height
          ) {
            elements.set(element, { width, height });
            hasChanged = true;
          }
        }
 
        if (hasChanged) {
          notify();
        }
      });
    }
 
    refCounts.set(rootElement, (refCounts.get(rootElement) ?? 0) + 1);
    refCounts.set(contentElement, (refCounts.get(contentElement) ?? 0) + 1);
 
    observer.observe(rootElement);
    observer.observe(contentElement);
 
    const rootRect = rootElement.getBoundingClientRect();
    const contentRect = contentElement.getBoundingClientRect();
 
    const rootData = { width: rootRect.width, height: rootRect.height };
    const contentData = {
      width: contentRect.width,
      height: contentRect.height,
    };
 
    elements.set(rootElement, rootData);
    elements.set(contentElement, contentData);
 
    if (
      rootData.width > 0 &&
      rootData.height > 0 &&
      contentData.width > 0 &&
      contentData.height > 0
    ) {
      notify();
    }
  }
 
  function unobserve(
    rootElement: RootElement | null,
    contentElement: Element | null,
  ) {
    if (!observer || !rootElement || !contentElement) return;
 
    const rootCount = (refCounts.get(rootElement) ?? 1) - 1;
    const contentCount = (refCounts.get(contentElement) ?? 1) - 1;
 
    if (rootCount <= 0) {
      observer.unobserve(rootElement);
      elements.delete(rootElement);
      refCounts.delete(rootElement);
    } else {
      refCounts.set(rootElement, rootCount);
    }
 
    if (contentCount <= 0) {
      observer.unobserve(contentElement);
      elements.delete(contentElement);
      refCounts.delete(contentElement);
    } else {
      refCounts.set(contentElement, contentCount);
    }
  }
 
  return {
    subscribe,
    getSnapshot,
    observe,
    unobserve,
  };
}
 
const resizeObserverStore = createResizeObserverStore();
 
function useResizeObserverStore(
  rootRef: React.RefObject<RootElement | null>,
  contentRef: React.RefObject<ContentElement | null>,
  orientation: Orientation,
) {
  const onSubscribe = React.useCallback(
    (callback: () => void) => resizeObserverStore.subscribe(callback),
    [],
  );
 
  const getSnapshot = React.useCallback(
    () =>
      resizeObserverStore.getSnapshot(
        rootRef.current,
        contentRef.current,
        orientation,
      ),
    [rootRef, contentRef, orientation],
  );
 
  return React.useSyncExternalStore(onSubscribe, getSnapshot, getSnapshot);
}
 
const DirectionContext = React.createContext<Direction | undefined>(undefined);
 
function useDirection(dir?: Direction): Direction {
  const contextDir = React.useContext(DirectionContext);
  return dir ?? contextDir ?? "ltr";
}
 
interface DivProps extends React.ComponentProps<"div"> {
  asChild?: boolean;
}
 
interface MarqueeContextValue {
  side: Side;
  orientation: Orientation;
  dir: Direction;
  speed: number;
  loopCount: number;
  contentRef: React.RefObject<ContentElement | null>;
  rootRef: React.RefObject<RootElement | null>;
  autoFill: boolean;
  pauseOnHover: boolean;
  pauseOnKeyboard: boolean;
  reverse: boolean;
  paused: boolean;
}
 
const MarqueeContext = React.createContext<MarqueeContextValue | null>(null);
 
function useMarqueeContext(consumerName: string) {
  const context = React.useContext(MarqueeContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
  }
  return context;
}
 
interface MarqueeRootProps extends DivProps {
  side?: Side;
  dir?: Direction;
  speed?: number;
  delay?: number;
  loopCount?: number;
  gap?: string | number;
  autoFill?: boolean;
  pauseOnHover?: boolean;
  pauseOnKeyboard?: boolean;
  reverse?: boolean;
}
 
function MarqueeRoot(props: MarqueeRootProps) {
  const {
    side = "left",
    dir: dirProp,
    speed = 50,
    delay = 0,
    loopCount = 0,
    gap = "1rem",
    asChild,
    autoFill = false,
    pauseOnHover = false,
    pauseOnKeyboard = false,
    reverse = false,
    className,
    style: styleProp,
    ref,
    ...marqueeProps
  } = props;
 
  const orientation: Orientation =
    side === "top" || side === "bottom" ? "vertical" : "horizontal";
 
  const dir = useDirection(dirProp);
 
  const rootRef = React.useRef<RootElement>(null);
  const contentRef = React.useRef<ContentElement>(null);
  const composedRef = useComposedRefs(ref, rootRef);
 
  const [paused, setPaused] = React.useState(false);
 
  const onKeyDown = React.useCallback(
    (event: React.KeyboardEvent) => {
      if (pauseOnKeyboard && event.key === " ") {
        event.preventDefault();
        setPaused((prev) => !prev);
      }
    },
    [pauseOnKeyboard],
  );
 
  const dimensions = useResizeObserverStore(rootRef, contentRef, orientation);
 
  const duration = React.useMemo(() => {
    const safeSpeed = Math.max(0.001, speed);
 
    if (!dimensions) {
      const defaultDistance = autoFill ? 1000 : 2000;
      return defaultDistance / safeSpeed;
    }
 
    const { rootSize, contentSize } = dimensions;
 
    if (autoFill) {
      const multiplier =
        contentSize < rootSize ? Math.ceil(rootSize / contentSize) : 1;
      return (contentSize * multiplier) / safeSpeed;
    } else {
      return contentSize < rootSize
        ? rootSize / safeSpeed
        : contentSize / safeSpeed;
    }
  }, [dimensions, speed, autoFill]);
 
  const style = React.useMemo<React.CSSProperties>(
    () => ({
      "--duration": `${duration}s`,
      "--gap": gap,
      "--delay": `${delay}s`,
      "--flip": dir === "rtl" ? "-1" : "1",
      "--loop-count":
        loopCount === 0 || loopCount === Infinity
          ? "infinite"
          : loopCount.toString(),
      ...styleProp,
    }),
    [duration, gap, delay, dir, loopCount, styleProp],
  );
 
  const contextValue = React.useMemo<MarqueeContextValue>(
    () => ({
      side,
      orientation,
      dir,
      speed,
      loopCount,
      contentRef,
      rootRef,
      autoFill,
      paused,
      pauseOnHover,
      pauseOnKeyboard,
      reverse,
    }),
    [
      side,
      orientation,
      dir,
      speed,
      loopCount,
      autoFill,
      paused,
      pauseOnHover,
      pauseOnKeyboard,
      reverse,
    ],
  );
 
  const MarqueePrimitive = asChild ? Slot : "div";
 
  return (
    <MarqueeContext.Provider value={contextValue}>
      <div data-slot="marquee-wrapper" className="grid">
        <MarqueePrimitive
          role="marquee"
          aria-live="off"
          data-orientation={orientation}
          data-slot="marquee"
          dir={dir}
          tabIndex={pauseOnKeyboard ? 0 : undefined}
          {...marqueeProps}
          ref={composedRef}
          className={cn(
            "relative flex overflow-hidden motion-reduce:animate-none",
            orientation === "vertical" && "h-full flex-col",
            orientation === "horizontal" && "w-full",
            paused && "[&_*]:[animation-play-state:paused]",
            pauseOnHover && "group",
            pauseOnKeyboard &&
              "rounded-md focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
            className,
          )}
          style={style}
          onKeyDown={pauseOnKeyboard ? onKeyDown : undefined}
        />
      </div>
    </MarqueeContext.Provider>
  );
}
 
const marqueeContentVariants = cva(
  "flex min-w-full shrink-0 [gap:var(--gap)]",
  {
    variants: {
      side: {
        left: "animate-marquee-left",
        right: "animate-marquee-right",
        top: "min-h-full min-w-auto animate-marquee-up flex-col",
        bottom: "min-h-full min-w-auto animate-marquee-down flex-col",
      },
      dir: {
        ltr: "",
        rtl: "",
      },
      pauseOnHover: {
        true: "group-hover:[animation-play-state:paused]",
        false: "",
      },
      reverse: {
        true: "[animation-direction:reverse]",
        false: "",
      },
    },
    compoundVariants: [
      {
        side: "left",
        dir: "rtl",
        className: "animate-marquee-left-rtl",
      },
      {
        side: "right",
        dir: "rtl",
        className: "animate-marquee-right-rtl",
      },
    ],
    defaultVariants: {
      side: "left",
      dir: "ltr",
      pauseOnHover: false,
      reverse: false,
    },
  },
);
 
function MarqueeContent(props: DivProps) {
  const {
    className,
    asChild,
    ref,
    children,
    style: styleProp,
    ...contentProps
  } = props;
 
  const context = useMarqueeContext(CONTENT_NAME);
  const composedRef = useComposedRefs(ref, context.contentRef);
 
  const isVertical = context.orientation === "vertical";
  const isRtl = context.dir === "rtl";
 
  const dimensions = useResizeObserverStore(
    context.rootRef,
    context.contentRef,
    context.orientation,
  );
 
  React.useEffect(() => {
    if (context.rootRef.current && context.contentRef.current) {
      resizeObserverStore.observe(
        context.rootRef.current,
        context.contentRef.current,
      );
 
      return () => {
        resizeObserverStore.unobserve(
          context.rootRef.current,
          context.contentRef.current,
        );
      };
    }
  }, [context.rootRef, context.contentRef]);
 
  const multiplier = React.useMemo(() => {
    if (!context.autoFill || !dimensions) return 1;
 
    const { rootSize, contentSize } = dimensions;
    if (contentSize === 0) return 1;
 
    return contentSize < rootSize ? Math.ceil(rootSize / contentSize) : 1;
  }, [context.autoFill, dimensions]);
 
  const onMultipliedChildrenRender = React.useCallback(
    (count: number) => {
      return Array.from({ length: Math.max(0, count) }).map((_, i) => (
        <React.Fragment key={i}>{children}</React.Fragment>
      ));
    },
    [children],
  );
 
  const style = React.useMemo(
    () => ({
      ...styleProp,
      animationDuration: "var(--duration)",
      animationDelay: "var(--delay)",
      animationIterationCount: "var(--loop-count)",
      animationDirection: context.reverse ? "reverse" : "normal",
    }),
    [styleProp, context.reverse],
  );
 
  const ContentPrimitive = asChild ? Slot : "div";
 
  return (
    <>
      <ContentPrimitive
        data-orientation={context.orientation}
        data-slot="marquee-content"
        {...contentProps}
        style={style}
        className={cn(
          marqueeContentVariants({
            side: context.side,
            dir: context.dir,
            pauseOnHover: context.pauseOnHover,
            reverse: context.reverse,
            className,
          }),
          isVertical && "flex-col",
          isVertical
            ? "mb-[var(--gap)]"
            : isRtl
              ? "ml-[var(--gap)]"
              : "mr-[var(--gap)]",
        )}
      >
        <div
          ref={composedRef}
          className={cn(
            "flex shrink-0 [gap:var(--gap)]",
            isVertical && "flex-col",
          )}
        >
          {children}
        </div>
        {onMultipliedChildrenRender(multiplier - 1)}
      </ContentPrimitive>
      <ContentPrimitive
        role="presentation"
        aria-hidden="true"
        {...contentProps}
        style={style}
        className={cn(
          marqueeContentVariants({
            side: context.side,
            dir: context.dir,
            pauseOnHover: context.pauseOnHover,
            reverse: context.reverse,
            className,
          }),
          isVertical && "flex-col",
        )}
      >
        {onMultipliedChildrenRender(multiplier)}
      </ContentPrimitive>
    </>
  );
}
 
function MarqueeItem(props: DivProps) {
  const { className, asChild, ...itemProps } = props;
 
  const ItemPrimitive = asChild ? Slot : "div";
 
  return (
    <ItemPrimitive
      data-slot="marquee-item"
      {...itemProps}
      className={cn("shrink-0", className)}
    />
  );
}
 
const marqueeEdgeVariants = cva("pointer-events-none absolute z-10", {
  variants: {
    side: {
      left: "top-0 left-0 h-full bg-gradient-to-r from-background to-transparent",
      right:
        "top-0 right-0 h-full bg-gradient-to-l from-background to-transparent",
      top: "top-0 left-0 w-full bg-gradient-to-b from-background to-transparent",
      bottom:
        "bottom-0 left-0 w-full bg-gradient-to-t from-background to-transparent",
    },
    size: {
      default: "",
      sm: "",
      lg: "",
    },
  },
  compoundVariants: [
    {
      side: ["left", "right"],
      size: "default",
      className: "w-1/4",
    },
    {
      side: ["left", "right"],
      size: "sm",
      className: "w-1/6",
    },
    {
      side: ["left", "right"],
      size: "lg",
      className: "w-1/3",
    },
    {
      side: ["top", "bottom"],
      size: "default",
      className: "h-1/4",
    },
    {
      side: ["top", "bottom"],
      size: "sm",
      className: "h-1/6",
    },
    {
      side: ["top", "bottom"],
      size: "lg",
      className: "h-1/3",
    },
  ],
  defaultVariants: {
    size: "default",
  },
});
 
interface MarqueeEdgeProps
  extends VariantProps<typeof marqueeEdgeVariants>,
    DivProps {}
 
function MarqueeEdge(props: MarqueeEdgeProps) {
  const { side, size, className, asChild, ...edgeProps } = props;
 
  const EdgePrimitive = asChild ? Slot : "div";
 
  return (
    <EdgePrimitive
      data-size={size}
      data-slot="marquee-edge"
      {...edgeProps}
      className={cn(marqueeEdgeVariants({ side, size, className }))}
    />
  );
}
 
export {
  MarqueeRoot as Root,
  MarqueeContent as Content,
  MarqueeItem as Item,
  MarqueeEdge as Edge,
  //
  MarqueeRoot as Marquee,
  MarqueeContent,
  MarqueeItem,
  MarqueeEdge,
};

Add the following CSS animations to your globals.css file:

:root {
  --animate-marquee-left: marquee-left var(--duration) linear var(--loop-count);
  --animate-marquee-right: marquee-right var(--duration) linear var(--loop-count);
  --animate-marquee-left-rtl: marquee-left-rtl var(--duration) linear var(--loop-count);
  --animate-marquee-right-rtl: marquee-right-rtl var(--duration) linear var(--loop-count);
  --animate-marquee-up: marquee-up var(--duration) linear var(--loop-count);
  --animate-marquee-down: marquee-down var(--duration) linear var(--loop-count);

@keyframes marquee-left {
  0% {
    transform: translateX(0%);
  }
  100% {
    transform: translateX(calc(-100% - var(--gap)));
  }
}

@keyframes marquee-right {
  0% {
    transform: translateX(calc(-100% - var(--gap)));
  }
  100% {
    transform: translateX(0%);
  }
}

@keyframes marquee-up {
  0% {
    transform: translateY(0%);
  }
  100% {
    transform: translateY(calc(-100% - var(--gap)));
  }
}

@keyframes marquee-down {
  0% {
    transform: translateY(calc(-100% - var(--gap)));
  }
  100% {
    transform: translateY(0%);
  }
}

@keyframes marquee-left-rtl {
  0% {
    transform: translateX(0%);
  }
  100% {
    transform: translateX(calc(100% + var(--gap)));
  }
}

@keyframes marquee-right-rtl {
  0% {
    transform: translateX(calc(100% + var(--gap)));
  }
  100% {
    transform: translateX(0%);
  }
}
}

Layout

Import the parts and compose them together.

import * as Marquee from "@/components/ui/marquee"

<Marquee.Root>
  <Marquee.Content>
    <Marquee.Item />
  </Marquee.Content>
  <Marquee.Edge side="left" />
  <Marquee.Edge side="right" />
</Marquee.Root>

Examples

Logo Showcase

Use the marquee to showcase logos or brands in a continuous scroll.

import {
  Marquee,
  MarqueeContent,
  MarqueeEdge,
  MarqueeItem,
} from "@/components/ui/marquee";
 
const companies = [
  {
    name: "Vercel",
    logo: (
      <svg width="24" height="24" viewBox="0 0 256 222" fill="currentColor">
        <path d="m128 0 128 221.705H0z" />
      </svg>
    ),
  },
  {
    name: "Next.js",
    logo: (
      <svg width="24" height="24" viewBox="0 0 180 180" fill="currentColor">
        <mask
          id="mask0_408_139"
          style={{ maskType: "alpha" }}
          maskUnits="userSpaceOnUse"
          x="0"
          y="0"
          width="180"
          height="180"
        >
          <circle cx="90" cy="90" r="90" fill="black" />
        </mask>
        <g mask="url(#mask0_408_139)">
          <circle
            cx="90"
            cy="90"
            r="87"
            fill="black"
            stroke="white"
            strokeWidth="6"
          />
          <path
            d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
            fill="url(#paint0_linear_408_139)"
          />
          <rect
            x="115"
            y="54"
            width="12"
            height="72"
            fill="url(#paint1_linear_408_139)"
          />
        </g>
        <defs>
          <linearGradient
            id="paint0_linear_408_139"
            x1="109"
            y1="116.5"
            x2="144.5"
            y2="160.5"
            gradientUnits="userSpaceOnUse"
          >
            <stop stopColor="white" />
            <stop offset="1" stopColor="white" stopOpacity="0" />
          </linearGradient>
          <linearGradient
            id="paint1_linear_408_139"
            x1="121"
            y1="54"
            x2="120.799"
            y2="106.875"
            gradientUnits="userSpaceOnUse"
          >
            <stop stopColor="white" />
            <stop offset="1" stopColor="white" stopOpacity="0" />
          </linearGradient>
        </defs>
      </svg>
    ),
  },
  {
    name: "React",
    logo: (
      <svg width="24" height="24" viewBox="0 0 569 512" fill="#58C4DC">
        <path d="M285.5,201 C255.400481,201 231,225.400481 231,255.5 C231,285.599519 255.400481,310 285.5,310 C315.599519,310 340,285.599519 340,255.5 C340,225.400481 315.599519,201 285.5,201" />
        <path d="M568.959856,255.99437 C568.959856,213.207656 529.337802,175.68144 466.251623,150.985214 C467.094645,145.423543 467.85738,139.922107 468.399323,134.521063 C474.621631,73.0415145 459.808523,28.6686204 426.709856,9.5541429 C389.677085,-11.8291748 337.36955,3.69129898 284.479928,46.0162134 C231.590306,3.69129898 179.282771,-11.8291748 142.25,9.5541429 C109.151333,28.6686204 94.3382249,73.0415145 100.560533,134.521063 C101.102476,139.922107 101.845139,145.443621 102.708233,151.02537 C97.4493791,153.033193 92.2908847,155.161486 87.3331099,157.39017 C31.0111824,182.708821 0,217.765415 0,255.99437 C0,298.781084 39.6220545,336.307301 102.708233,361.003527 C101.845139,366.565197 101.102476,372.066633 100.560533,377.467678 C94.3382249,438.947226 109.151333,483.32012 142.25,502.434597 C153.629683,508.887578 166.52439,512.186771 179.603923,511.991836 C210.956328,511.991836 247.567589,495.487529 284.479928,465.972527 C321.372196,495.487529 358.003528,511.991836 389.396077,511.991836 C402.475265,512.183856 415.36922,508.884856 426.75,502.434597 C459.848667,483.32012 474.661775,438.947226 468.439467,377.467678 C467.897524,372.066633 467.134789,366.565197 466.291767,361.003527 C529.377946,336.347457 569,298.761006 569,255.99437 M389.155214,27.1025182 C397.565154,26.899606 405.877839,28.9368502 413.241569,33.0055186 C436.223966,46.2772304 446.540955,82.2775015 441.522965,131.770345 C441.181741,135.143488 440.780302,138.556788 440.298575,141.990165 C414.066922,134.08804 387.205771,128.452154 360.010724,125.144528 C343.525021,103.224055 325.192524,82.7564475 305.214266,63.9661533 C336.586743,39.7116483 366.032313,27.1025182 389.135142,27.1025182 M378.356498,310.205598 C368.204912,327.830733 357.150626,344.919965 345.237759,361.405091 C325.045049,363.479997 304.758818,364.51205 284.459856,364.497299 C264.167589,364.51136 243.888075,363.479308 223.702025,361.405091 C211.820914,344.919381 200.80007,327.83006 190.683646,310.205598 C180.532593,292.629285 171.306974,274.534187 163.044553,255.99437 C171.306974,237.454554 180.532593,219.359455 190.683646,201.783142 C200.784121,184.229367 211.770999,167.201087 223.601665,150.764353 C243.824636,148.63809 264.145559,147.579168 284.479928,147.591877 C304.772146,147.579725 325.051559,148.611772 345.237759,150.68404 C357.109048,167.14607 368.136094,184.201112 378.27621,201.783142 C388.419418,219.363718 397.644825,237.458403 405.915303,255.99437 C397.644825,274.530337 388.419418,292.625022 378.27621,310.205598 M419.724813,290.127366 C426.09516,307.503536 431.324985,325.277083 435.380944,343.334682 C417.779633,348.823635 399.836793,353.149774 381.668372,356.285142 C388.573127,345.871232 395.263781,335.035679 401.740334,323.778483 C408.143291,312.655143 414.144807,301.431411 419.805101,290.207679 M246.363271,390.377981 C258.848032,391.140954 271.593728,391.582675 284.5,391.582675 C297.406272,391.582675 310.232256,391.140954 322.737089,390.377981 C310.880643,404.583418 298.10766,417.997563 284.5,430.534446 C270.921643,417.999548 258.18192,404.585125 246.363271,390.377981 Z M187.311556,356.244986 C169.137286,353.123646 151.187726,348.810918 133.578912,343.334682 C137.618549,325.305649 142.828222,307.559058 149.174827,290.207679 C154.754833,301.431411 160.736278,312.655143 167.239594,323.778483 C173.74291,334.901824 180.467017,345.864539 187.311556,356.285142 M149.174827,221.760984 C142.850954,204.473938 137.654787,186.794745 133.619056,168.834762 C151.18418,163.352378 169.085653,159.013101 187.211197,155.844146 C180.346585,166.224592 173.622478,176.986525 167.139234,188.210257 C160.65599,199.433989 154.734761,210.517173 149.074467,221.760984 M322.616657,121.590681 C310.131896,120.827708 297.3862,120.385987 284.379568,120.385987 C271.479987,120.385987 258.767744,120.787552 246.242839,121.590681 C258.061488,107.383537 270.801211,93.9691137 284.379568,81.4342157 C297.99241,93.9658277 310.765727,107.380324 322.616657,121.590681 Z M401.70019,188.210257 C395.196875,176.939676 388.472767,166.09743 381.527868,155.68352 C399.744224,158.819049 417.734224,163.151949 435.380944,168.654058 C431.331963,186.680673 426.122466,204.426664 419.785029,221.781062 C414.205023,210.55733 408.203506,199.333598 401.720262,188.230335 M127.517179,131.790423 C122.438973,82.3176579 132.816178,46.2973086 155.778503,33.0255968 C163.144699,28.9632474 171.455651,26.9264282 179.864858,27.1225964 C202.967687,27.1225964 232.413257,39.7317265 263.785734,63.9862316 C243.794133,82.7898734 225.448298,103.270812 208.949132,125.204763 C181.761691,128.528025 154.90355,134.14313 128.661281,141.990165 C128.199626,138.556788 127.778115,135.163566 127.456963,131.790423 M98.4529773,182.106474 C101.54406,180.767925 104.695358,179.429376 107.906872,178.090828 C114.220532,204.735668 122.781793,230.7969 133.498624,255.99437 C122.761529,281.241316 114.193296,307.357063 107.8868,334.058539 C56.7434387,313.076786 27.0971497,284.003505 27.0971497,255.99437 C27.0971497,229.450947 53.1907013,202.526037 98.4529773,182.106474 Z M155.778503,478.963143 C132.816178,465.691432 122.438973,429.671082 127.517179,380.198317 C127.838331,376.825174 128.259842,373.431953 128.721497,369.978497 C154.953686,377.878517 181.814655,383.514365 209.009348,386.824134 C225.500295,408.752719 243.832321,429.233234 263.805806,448.042665 C220.069,481.834331 180.105722,492.97775 155.838719,478.963143 M441.502893,380.198317 C446.520883,429.691161 436.203894,465.691432 413.221497,478.963143 C388.974566,493.017906 348.991216,481.834331 305.274481,448.042665 C325.241364,429.232737 343.566681,408.752215 360.050868,386.824134 C387.245915,383.516508 414.107066,377.880622 440.338719,369.978497 C440.820446,373.431953 441.221885,376.825174 441.563109,380.198317 M461.193488,334.018382 C454.869166,307.332523 446.294494,281.231049 435.561592,255.99437 C446.289797,230.744081 454.857778,204.629101 461.173416,177.930202 C512.216417,198.911955 541.942994,227.985236 541.942994,255.99437 C541.942994,284.003505 512.296705,313.076786 461.153344,334.058539" />
      </svg>
    ),
  },
  {
    name: "TypeScript",
    logo: (
      <svg width="24" height="24" viewBox="0 0 256 256" fill="currentColor">
        <path
          d="M20 0h216c11.046 0 20 8.954 20 20v216c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20V20C0 8.954 8.954 0 20 0Z"
          fill="#3178C6"
        />
        <path
          d="M150.518 200.475v27.62c4.492 2.302 9.805 4.028 15.938 5.179 6.133 1.151 12.597 1.726 19.393 1.726 6.622 0 12.914-.633 18.874-1.899 5.96-1.266 11.187-3.352 15.678-6.257 4.492-2.906 8.048-6.704 10.669-11.394 2.62-4.689 3.93-10.486 3.93-17.391 0-5.006-.749-9.394-2.246-13.163a30.748 30.748 0 0 0-6.479-10.055c-2.821-2.935-6.205-5.567-10.149-7.898-3.945-2.33-8.394-4.531-13.347-6.602-3.628-1.497-6.881-2.949-9.761-4.359-2.879-1.41-5.327-2.848-7.342-4.316-2.016-1.467-3.571-3.021-4.665-4.661-1.094-1.64-1.641-3.495-1.641-5.567 0-1.899.489-3.61 1.468-5.135s2.362-2.834 4.147-3.927c1.785-1.094 3.973-1.942 6.565-2.547 2.591-.604 5.471-.906 8.638-.906 2.304 0 4.737.173 7.299.518 2.563.345 5.14.877 7.732 1.597a53.669 53.669 0 0 1 7.558 2.719 41.7 41.7 0 0 1 6.781 3.797v-25.807c-4.204-1.611-8.797-2.805-13.778-3.582-4.981-.777-10.697-1.165-17.147-1.165-6.565 0-12.784.705-18.658 2.115-5.874 1.409-11.043 3.61-15.506 6.602-4.463 2.993-7.99 6.805-10.582 11.437-2.591 4.632-3.887 10.17-3.887 16.615 0 8.228 2.375 15.248 7.127 21.06 4.751 5.811 11.963 10.731 21.638 14.759a291.458 291.458 0 0 1 10.625 4.575c3.283 1.496 6.119 3.049 8.509 4.66 2.39 1.611 4.276 3.366 5.658 5.265 1.382 1.899 2.073 4.057 2.073 6.474a9.901 9.901 0 0 1-1.296 4.963c-.863 1.524-2.174 2.848-3.93 3.97-1.756 1.122-3.945 1.999-6.565 2.632-2.62.633-5.687.95-9.2.95-5.989 0-11.92-1.05-17.794-3.151-5.875-2.1-11.317-5.25-16.327-9.451Zm-46.036-68.733H140V109H41v22.742h35.345V233h28.137V131.742Z"
          fill="#FFF"
        />
      </svg>
    ),
  },
  {
    name: "Tailwind",
    logo: (
      <svg width="24" height="24" viewBox="0 0 54 33" fill="#38bdf8">
        <g clipPath="url(#a)">
          <path
            fillRule="evenodd"
            d="M27 0c-7.2 0-11.7 3.6-13.5 10.8 2.7-3.6 5.85-4.95 9.45-4.05 2.054.513 3.522 2.004 5.147 3.653C30.744 13.09 33.808 16.2 40.5 16.2c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C36.756 3.11 33.692 0 27 0zM13.5 16.2C6.3 16.2 1.8 19.8 0 27c2.7-3.6 5.85-4.95 9.45-4.05 2.054.514 3.522 2.004 5.147 3.653C17.244 29.29 20.308 32.4 27 32.4c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C23.256 19.31 20.192 16.2 13.5 16.2z"
            clipRule="evenodd"
          />
        </g>
      </svg>
    ),
  },
  {
    name: "GitHub",
    logo: (
      <svg width="24" height="24" viewBox="0 0 1024 1024" fill="currentColor">
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
          transform="scale(64)"
        />
      </svg>
    ),
  },
];
 
export function MarqueeLogoDemo() {
  return (
    <Marquee autoFill>
      <MarqueeContent>
        {companies.map((company) => (
          <MarqueeItem key={company.name}>
            <div className="flex size-16 items-center justify-center rounded-full bg-accent">
              {company.logo}
              <span className="sr-only">{company.name}</span>
            </div>
          </MarqueeItem>
        ))}
      </MarqueeContent>
      <MarqueeEdge side="left" />
      <MarqueeEdge side="right" />
    </Marquee>
  );
}

Vertical Layout

Use side to control the direction of the marquee.

"use client";
 
import {
  Marquee,
  MarqueeContent,
  MarqueeEdge,
  MarqueeItem,
} from "@/components/ui/marquee";
 
const testimonials = [
  {
    name: "Alex Johnson",
    role: "Frontend Developer",
    company: "TechCorp",
    content:
      "This component library has transformed our development workflow. The quality and attention to detail is outstanding.",
    avatar: "AJ",
  },
  {
    name: "Sarah Chen",
    role: "Design Lead",
    company: "StartupXYZ",
    content:
      "Beautiful components that are easy to customize. Our design system has never looked better.",
    avatar: "SC",
  },
  {
    name: "Michael Rodriguez",
    role: "Full Stack Engineer",
    company: "WebSolutions",
    content:
      "The accessibility features built into these components saved us weeks of development time.",
    avatar: "MR",
  },
  {
    name: "Emily Davis",
    role: "Product Manager",
    company: "InnovateLab",
    content:
      "Our team productivity increased significantly after adopting this component library.",
    avatar: "ED",
  },
  {
    name: "David Kim",
    role: "Senior Developer",
    company: "CodeCraft",
    content:
      "Clean, modern components with excellent TypeScript support. Highly recommended!",
    avatar: "DK",
  },
  {
    name: "Lisa Thompson",
    role: "UI/UX Designer",
    company: "DesignStudio",
    content:
      "The design tokens and theming system make it incredibly easy to maintain brand consistency.",
    avatar: "LT",
  },
];
 
export function MarqueeVerticalDemo() {
  return (
    <Marquee side="bottom" className="h-[320px]">
      <MarqueeContent>
        {testimonials.map((testimonial) => (
          <MarqueeItem key={testimonial.name} asChild>
            <div className="flex w-full flex-col gap-3 rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
              <div className="flex items-center gap-3">
                <div className="flex size-10 items-center justify-center rounded-full bg-primary font-medium text-primary-foreground text-sm">
                  {testimonial.avatar}
                </div>
                <div className="flex flex-col">
                  <div className="font-medium text-sm">{testimonial.name}</div>
                  <div className="text-muted-foreground text-xs">
                    {testimonial.role} at {testimonial.company}
                  </div>
                </div>
              </div>
              <p className="text-muted-foreground text-sm leading-relaxed">
                "{testimonial.content}"
              </p>
            </div>
          </MarqueeItem>
        ))}
      </MarqueeContent>
      <MarqueeEdge side="top" />
      <MarqueeEdge side="bottom" />
    </Marquee>
  );
}

With RTL

The marquee component automatically adapts to RTL (right-to-left) layouts.

"use client";
 
import {
  Marquee,
  MarqueeContent,
  MarqueeEdge,
  MarqueeItem,
} from "@/components/ui/marquee";
 
const features = [
  {
    title: "RTL Support",
    description:
      "Automatic right-to-left layout support with proper animation direction and gap handling.",
  },
  {
    title: "Smooth Animation",
    description:
      "Seamless scrolling animation that adapts to text direction without any visual gaps.",
  },
  {
    title: "Auto Fill",
    description:
      "Intelligent content duplication to fill the available space for continuous scrolling.",
  },
  {
    title: "Pause on Hover",
    description:
      "Interactive animation that pauses when users hover over the content for better UX.",
  },
  {
    title: "Responsive Design",
    description:
      "Fully responsive component that works perfectly across all device sizes and orientations.",
  },
];
 
export function MarqueeRtlDemo() {
  return (
    <Marquee dir="rtl">
      <MarqueeContent>
        {features.map((feature) => (
          <MarqueeItem key={feature.title} asChild>
            <div className="flex w-[280px] flex-col gap-1 rounded-md border bg-card p-4 text-card-foreground shadow-sm">
              <div className="font-medium text-sm leading-tight sm:text-base">
                {feature.title}
              </div>
              <span className="line-clamp-2 text-muted-foreground text-sm">
                {feature.description}
              </span>
            </div>
          </MarqueeItem>
        ))}
      </MarqueeContent>
      <MarqueeEdge side="left" />
      <MarqueeEdge side="right" />
    </Marquee>
  );
}

API Reference

Marquee

The main marquee component that creates continuous scrolling animations.

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

MarqueeContent

Contains the scrolling content and handles repetition for seamless animation.

Prop

Type

Data AttributeValue
[data-orientation]"horizontal" | "vertical"

MarqueeItem

Individual items within the marquee content.

Prop

Type

MarqueeEdge

Edge overlay components for smooth gradient transitions.

Prop

Type

Data AttributeValue
[data-size]"default" | "sm" | "lg"

Accessibility

Keyboard Interactions

KeyDescription
SpacePauses or resumes the marquee animation when pauseOnKeyboard is enabled.

Features

  • RTL Support: Automatically adapts to RTL (right-to-left) layouts
  • Screen Reader Support: Content remains accessible to assistive technologies
  • Reduced Motion: Respects user's prefers-reduced-motion setting
  • Pause Controls:
    • Hover: Can be configured to pause animation when hovered
    • Keyboard: Press Space key to pause/resume (when pauseOnKeyboard is enabled)
  • Focus Management: Proper focus indicators and keyboard navigation when pauseOnKeyboard is enabled