Random UI

Markee

A powerful and composable marquee component built with compound components pattern

Tailwind CSS
Tailwind CSS
CSS
CSS
React
React
NextJS
NextJS

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

  • NextJS
    NextJS
  • GSAP
    GSAP
  • React
    React
  • TypeScript
    TypeScript
  • Framer Motion
    Framer Motion
  • Tailwind CSS
    Tailwind CSS
  • CSS
    CSS

Installation

Copy and paste the following code into your project.

components/markee.tsx
"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
        )}
        role="region"
        aria-label="Marquee content"
        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"
      aria-label="Marquee spacer"
      {...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"
        role="list"
        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"
        {...props}
        aria-hidden="true"
      />
    </div>
  </MarkeeContentContext.Provider>;
}
MarkeeContent.displayName = "MarkeeContent";

function MarkeeItem({ ...props }: React.ComponentProps<"li">) {
  return <li 
  data-slot="markee-item"
  role="listitem" 
  aria-label="Marquee item"
  {...props} />;
}
MarkeeItem.displayName = "MarkeeItem";

export { Markee, MarkeeSpacer, MarkeeFade, MarkeeContent, MarkeeItem };
export type { MarkeeContentProps };

Add the following CSS to your global stylesheet:

app/global.css
@theme {
  --animate-markee-scroll: markee-scroll;

  @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

  • NextJS
    NextJS
  • GSAP
    GSAP
  • React
    React
  • TypeScript
    TypeScript
  • Framer Motion
    Framer Motion
  • Tailwind CSS
    Tailwind CSS
  • CSS
    CSS
  • NextJS
    NextJS
  • GSAP
    GSAP
  • React
    React
  • TypeScript
    TypeScript
  • Framer Motion
    Framer Motion
  • Tailwind CSS
    Tailwind CSS
  • CSS
    CSS
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 "framer-motion";
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.