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
- Next.js
- GSAP
- React
- TypeScript
- Motion
- Tailwind CSS
- CSS
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:
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.
| 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.