Markee
A powerful and composable marquee component built with compound components pattern




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

- GSAP

- React

- TypeScript

- Framer Motion
- Tailwind CSS

- CSS

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
)}
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:
@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:
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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Must contain MarkeeContent and optionally MarkeeFade components |
MarkeeContent
Component that renders the scrolling content. Must be used inside Markee. Accepts all ul element props.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "left" | "right" | "left" | Direction of the marquee |
duration | number | 10 | Animation duration in seconds |
pauseOnHover | boolean | false | Whether to pause animation on hover |
paused | boolean | false | Whether to pause the animation |
ease | "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "linear" | CSS animation timing function |
className | string | - | Additional CSS classes |
children | ReactNode | - | Content items (MarkeeItem and MarkeeSpacer) |
MarkeeItem
Component for individual items in the marquee. Must be used inside MarkeeContent. Accepts all li element props.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Item content |
className | string | - | Additional CSS classes |
MarkeeSpacer
Component for adding spacing or dividers between items. Must be used inside MarkeeContent.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
position | "left" | "right" | - | Fade position (required) |
className | string | - | Additional CSS classes. Default includes w-12 |
Accessibility
The component follows accessibility standards and ARIA best practices.