Random Access Components

Markee

A powerful and composable marquee component built with compound components pattern

CSS iconCSS
TailwindCSS iconTailwind CSS
React IconReact
Next.js IconNext.js
Gsap iconGSAP
Motion iconMotion

A performant, accessible marquee component built with React compound components. Features customizable animation speed, easing options, pause on hover, and optional fade effects. Fully responsive, width-adaptive, and WCAG compliant.

It also supports GSAP and Framer Motion integration out of the box! See the dedicated section below.

Demo

Markee

  • Next.js IconNext.js
  • Gsap iconGSAP
  • React IconReact
  • TypeScript iconTypeScript
  • Motion iconMotion
  • TailwindCSS iconTailwind CSS
  • CSS iconCSS
const BADGES = [
<TechBadge badge="nextjs" />,
<TechBadge badge="gsap" />,
<TechBadge badge="react" />,
<TechBadge badge="typescript" />,
<TechBadge badge="motion" />,
<TechBadge badge="tailwindcss" />,
<TechBadge badge="css" />,
]

const MarkeeDemo: React.FC = () => {
const [showFades, setShowFades] = React.useState(true);
const [pauseOnHover, setPauseOnHover] = React.useState(false);
const [direction, setDirection] = React.useState<"left" | "right">("left");
const [duration, setDuration] = React.useState(10);
const [paused, setPaused] = React.useState(false);

return (
  <DemoBlock className="m-0" containerClassName="flex flex-col items-center justify-center h-[400px]">
    <h2 className="mt-0 text-4xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-foreground">
        Markee
    </h2>
    <Markee className="w-full md:w-3/4">
      {showFades && <MarkeeFade position="left" className="from-background" />}
      <MarkeeContent duration={duration} pauseOnHover={pauseOnHover} direction={direction} paused={paused}>
        {BADGES.map((badge, index) => (
          <React.Fragment key={index}>
            <MarkeeItem className="list-none">
              {badge}
            </MarkeeItem>
            <MarkeeSpacer className="w-2 md:w-4" />
          </React.Fragment>
        ))}
      </MarkeeContent>
      {showFades && <MarkeeFade position="right" className="from-background" />}
    </Markee>
    <div className="mt-12 flex gap-6 flex-wrap w-full justify-center">
      <div className="flex items-center gap-2">
        <Checkbox id="fades" checked={showFades} onCheckedChange={() => setShowFades(!showFades)} />
        <Label htmlFor="fades" className="text-sm text-muted-foreground font-normal">Fades</Label>
      </div>
      <div className="flex items-center gap-2">
        <Checkbox id="pauseOnHover" checked={pauseOnHover} onCheckedChange={() => setPauseOnHover(!pauseOnHover)} />
        <Label htmlFor="pauseOnHover" className="text-sm text-muted-foreground font-normal">Pause on hover</Label>
      </div>
      <div className="flex items-center gap-2">
        <Checkbox id="paused" checked={paused} onCheckedChange={() => setPaused(!paused)} />
        <Label htmlFor="paused" className="text-sm text-muted-foreground font-normal">Paused</Label>
      </div>
      <Button variant="secondary" className="flex items-center gap-2" onClick={() => setDirection(direction === "left" ? "right" : "left")}>
        <span className="text-sm text-muted-foreground font-normal">Direction</span>
        <ArrowLeftRight className="size-4 text-muted-foreground" />
      </Button>

      <div className="flex items-center gap-2">
        <Label htmlFor="duration" className="text-sm text-muted-foreground font-normal">Duration</Label>
        <InputGroup className="w-24">
        <InputGroupInput 
          placeholder="Duration" 
          value={duration} 
          readOnly 
        />
        <InputGroupAddon align="inline-end">
          <Button variant="secondary" className="w-5 !p-0" onClick={() => setDuration(duration - 1)}>-</Button>
          <Button variant="secondary" className="w-5 !p-0" onClick={() => setDuration(duration + 1)}>+</Button>
        </InputGroupAddon>
      </InputGroup>

      </div>
    </div>
  </DemoBlock>
);
}

Installation

Copy and paste the following code into your project.

"use client";import * as React from "react";import { cn } from "@/lib/utils";const markeeContext = React.createContext<boolean>(false);const markeeContentContext = React.createContext<boolean>(false);function Markee({ className, ...props }: React.ComponentProps<"div">) {return (	<markeeContext.Provider value={true}>		<div			data-slot="markee"			className={cn("relative flex overflow-hidden max-w-fit", className)}			aria-live="polite"			{...props}		/>	</markeeContext.Provider>);}Markee.displayName = "Markee";function MarkeeFade({className,position,...props}: React.ComponentProps<"div"> & { position: "left" | "right" }) {const isInMarkee = React.useContext(markeeContext);if (!isInMarkee) {	console.error("MarkeeFade must be used inside a Markee component");	return null;}return (	<div		aria-hidden="true"		data-slot="markee-fade"		className={cn(			"absolute top-0 h-full w-12 z-10 pointer-events-none",			position === "left"				? "left-0 bg-gradient-to-r from-background to-transparent"				: "right-0 bg-gradient-to-l from-background to-transparent",			className		)}		{...props}	/>);}MarkeeFade.displayName = "MarkeeFade";function MarkeeSpacer({ className, ...props }: React.ComponentProps<"div">) {return (	<div		data-slot="markee-spacer"		className={cn("shrink-0 w-4", className)}		aria-hidden="true"		{...props}	/>);}MarkeeSpacer.displayName = "MarkeeSpacer";interface MarkeeContentProps extends React.ComponentProps<"ul"> {/** * Direction of the animation. * @default "left" (left to right) */direction?: "left" | "right";/** * Duration in seconds of the animation. * Higher values result in slower animation and vice versa. * @default 10 */duration?: number;/** * Animation easing * @default "linear" */ease?: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out";/** * Whether to pause the animation on hover * @default false */pauseOnHover?: boolean;/** * Whether the markee is paused or not * @default false */paused?: boolean;}function MarkeeContent({duration = 10,ease = "linear",direction = "left",pauseOnHover = false,paused = false,className,...props}: MarkeeContentProps) {const animationStyle = React.useMemo(	() => ({		animationDuration: `${duration}s`,		animationTimingFunction: ease,		animationDirection: direction === "left" ? ("normal" as const) : ("reverse" as const),	}),	[duration, ease, direction]);const isInMarkee = React.useContext(markeeContext);if (!isInMarkee) {	console.error("MarkeeContent must be used inside a Markee component");	return null;}return (	<markeeContentContext.Provider value={true}>		<div			data-slot="markee-content-wrapper"			style={{ ...animationStyle }}			className={cn(				"relative flex shrink-0 animate-markee-scroll [animation-iteration-count:infinite] motion-reduce:[animation-play-state:paused",				pauseOnHover && "hover:[animation-play-state:paused]",				paused && "[animation-play-state:paused]",				className			)}		>			<ul				data-slot="markee-content"				className="flex shrink-0 justify-around min-w-full pl-0!"				aria-label="Marquee content list"				{...props}			/>			<ul				data-slot="markee-content-hidden"				className="flex shrink-0 justify-around min-w-full absolute top-0 left-full pl-0!"				{...props}				aria-hidden="true"			/>		</div>	</markeeContentContext.Provider>);}MarkeeContent.displayName = "MarkeeContent";function MarkeeItem({ ...props }: React.ComponentProps<"li">) {return <li data-slot="markee-item" aria-label="Marquee item" {...props} />;}MarkeeItem.displayName = "MarkeeItem";export { Markee, MarkeeSpacer, MarkeeFade, MarkeeContent, MarkeeItem };export type { MarkeeContentProps };
@keyframes markee-scroll {  from {    transform: translateX(0);  }  to {    transform: translateX(-100%);  }}

Usage

import { 
  Markee, 
  MarkeeContent, 
  MarkeeSpacer, 
  MarkeeFade,
  MarkeeItem
} from "@/components/markee";

Basic

<Markee>
  <MarkeeFade position="left" />
  <MarkeeContent>
    <MarkeeItem>Item 1</MarkeeItem>
    <MarkeeSpacer />
    <MarkeeItem>Item 2</MarkeeItem>
    <MarkeeSpacer />
    <MarkeeItem>Item 3</MarkeeItem>
  </MarkeeContent>
  <MarkeeFade position="right" />
</Markee>

Dynamic Content

const items = ["Item 1", "Item 2", "Item 3"];

<Markee>
  <MarkeeFade position="left" />
  <MarkeeContent>
    {items.map((item, i) => (
      <React.Fragment key={i}>
        <MarkeeItem>{item}</MarkeeItem>
        <MarkeeSpacer className="w-4" />
      </React.Fragment>
    ))}
  </MarkeeContent>
  <MarkeeFade position="right" />
</Markee>

Custom Markee Width

<Markee className="w-full md:w-1/2 lg:w-1/3">
  <MarkeeFade position="left" />
  <MarkeeContent>
    <MarkeeItem>Item 1</MarkeeItem>
    <MarkeeSpacer />
    <MarkeeItem>Item 2</MarkeeItem>
    <MarkeeSpacer />
    <MarkeeItem>Item 3</MarkeeItem>
  </MarkeeContent>
  <MarkeeFade position="right" />
</Markee>

Custom Fades

<Markee>
  <MarkeeFade position="left" className="w-16 from-lime to-transparent" />
  <MarkeeContent>
    ...
  </MarkeeContent>
  <MarkeeFade position="right" className="w-16 from-lime to-transparent" />
</Markee>

Custom Spacers

<Markee>
  <MarkeeContent>
    <MarkeeItem>Item 1</MarkeeItem>
    <MarkeeSpacer className="w-4 md:w-12" /> {/* Custom width */} 
    <MarkeeItem>Item 2</MarkeeItem>
    <MarkeeSpacer> {/* Or with custom content inside */}
      <MyDivider />
    </MarkeeSpacer>
    <MarkeeItem>Item 3</MarkeeItem>
  </MarkeeContent>
</Markee>

Animation Integration

Gsap

You can integrate Gsap out of the box, without editing directly the component, in the following way:

Scroll to see the animation

Markee

  • Next.js IconNext.js
  • Gsap iconGSAP
  • React IconReact
  • TypeScript iconTypeScript
  • Motion iconMotion
  • TailwindCSS iconTailwind CSS
  • CSS iconCSS
  • Next.js IconNext.js
  • Gsap iconGSAP
  • React IconReact
  • TypeScript iconTypeScript
  • Motion iconMotion
  • TailwindCSS iconTailwind CSS
  • CSS iconCSS
    import { useGSAP } from "@gsap/react";
    import gsap from "gsap";
    import { ScrollTrigger } from "gsap/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);
useGSAP(()=>{

  gsap.set(".mymarkee-content-right", { translateX: "-100%" });

  const tl = gsap.timeline({
    scrollTrigger: {
      // Your scroll trigger configuration
      start: 'top top',
      end: 'bottom top',
      scrub: true,
    },
  });

  tl.to(".mymarkee-content", {
    translateX: "-10%",
    ease: 'power2.inOut',
  });

  tl.to(".mymarkee-content-right", {
    translateX: "-90%",
    ease: 'power2.inOut',
  },'<');

});
return (
  <Markee>
    <MarkeeFade position="left" />
    <MarkeeContent className="mymarkee-content" paused>
      ...
    </MarkeeContent>
    <MarkeeFade position="right" />
  </Markee>

  <Markee>
    <MarkeeFade position="left" />
    <MarkeeContent className="mymarkee-content-right" paused direction="right">
      ...
    </MarkeeContent>
    <MarkeeFade position="right" />
  </Markee>
);

Framer Motion

Also Framer motion integration is supported out of the box, without editing directly the component, in the following way:

import { motion, useScroll, useTransform } from "motion/react";
const { scrollYProgress } = useScroll({ offset: ["start center", "end end"] });
const translateX = useTransform(scrollYProgress, [0, 1], ["-50%", "0%"]);
return (
  <Markee>
    <MarkeeFade position="left" />
    <MarkeeContent paused>
      {/* Wrap all content inside a motion.div, Set flex display to keep the layout! */}
      <motion.div style={{ translateX }} className="flex">
        {badges.map((badge, index) => (
          <Fragment key={index}>
            <MarkeeItem>
              {badge}
            </MarkeeItem>
            <MarkeeSpacer className="w-4" />
          </Fragment>
        ))}
      </motion.div>
    </MarkeeContent>
    <MarkeeFade position="right" />
  </Markee>
);

Limitations for GSAP & Framer Motion

Due to the nature of the marquee animation, I recommend you to not scroll for the entire marquee width, but rather a smaller portion of it, otherwise it will results in having an empty space at the start/end of the marquee.

API Reference

Markee

The root component that wraps the marquee functionality.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode-Must contain MarkeeContent and optionally MarkeeFade components

MarkeeContent

Component that renders the scrolling content. Must be used inside Markee. Accepts all ul element props.

PropTypeDefaultDescription
direction"left" | "right""left"Direction of the marquee
durationnumber10Animation duration in seconds
pauseOnHoverbooleanfalseWhether to pause animation on hover
pausedbooleanfalseWhether to pause the animation
ease"linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out""linear"CSS animation timing function
classNamestring-Additional CSS classes
childrenReactNode-Content items (MarkeeItem and MarkeeSpacer)

MarkeeItem

Component for individual items in the marquee. Must be used inside MarkeeContent. Accepts all li element props.

PropTypeDefaultDescription
childrenReactNode-Item content
classNamestring-Additional CSS classes

MarkeeSpacer

Component for adding spacing or dividers between items. Must be used inside MarkeeContent.

PropTypeDefaultDescription
classNamestring-Additional CSS classes. Default includes shrink-0 w-4

MarkeeFade

Component for adding fade effects on the edges. Must be used inside Markee as a direct child.

PropTypeDefaultDescription
position"left" | "right"-Fade position (required)
classNamestring-Additional CSS classes. Default includes w-12

Accessibility

The component follows accessibility standards and ARIA best practices.

On this page