1
3
Block

Marquee

PreviousNext

A customizable, interactive scrolling marquee component with various animation options, and responsive design.

import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Marquee } from "@/components/ui/marquee"

const features = [
  {
    avatar:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
    title: "John Mark",
    description: "Unlock data-driven decisions with precision analytics.",
    badge: "Frontend",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
    title: "Thomas Shelby",
    description: "Experience seamless performance at scale.",
    badge: "Designer",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
    title: "Larry Page",
    description: "Enterprise-grade protection for your assets.",
    badge: "Expert",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
    title: "Berry White",
    description: "Connect across borders with effortless integration.",
    badge: "Lawyer",
  },
  {
    avatar:
      "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
    title: "Susan Collins",
    description: "Visualize trends and forecasts in real-time.",
    badge: "Manager",
  },
]

const FeatureCard = ({
  avatar,
  title,
  description,
  badge,
}: {
  avatar: string
  title: string
  description: string
  badge: string
}) => {
  return (
    <div
      className={cn(
        "relative h-full w-74 cursor-pointer overflow-hidden rounded-2xl border p-6 transition-all duration-200 hover:scale-95",
        // light styles
        "border-border bg-card hover:bg-accent/20",
        // dark styles
        "dark:border-border dark:bg-card dark:hover:bg-accent/20"
      )}
    >
      <div className="mb-4 text-4xl">
        <Avatar className="h-12 w-12">
          <AvatarImage src={avatar} alt={title} />
          <AvatarFallback className="bg-primary text-primary-foreground">
            {title.charAt(0)}
          </AvatarFallback>
        </Avatar>
      </div>
      <div className="space-y-2">
        <div className="flex items-center justify-between">
          <h3 className="text-foreground text-lg font-semibold">{title}</h3>
          <Badge variant="secondary" className="text-xs">
            {badge}
          </Badge>
        </div>
        <p className="text-muted-foreground text-sm">{description}</p>
      </div>
    </div>
  )
}

export function FeatureMarqueeDemo() {
  return (
    <div className="relative flex w-full flex-col items-center justify-center overflow-hidden">
      <Marquee pauseOnHover className="w-full max-w-6xl [--gap:1.5rem]">
        {features.map((feature, index) => (
          <FeatureCard key={index} {...feature} />
        ))}
      </Marquee>
    </div>
  )
}

Installation

pnpm dlx shadcn@latest add https://ebonui.com/r/marquee.json

Examples

Vertical

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Marquee } from "@/components/ui/marquee"

const teamMembers = [
  {
    name: "Alex Rivera",
    role: "Lead Designer",
    bio: "Crafting pixel-perfect experiences that delight users worldwide.",
    avatar:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
    badge: "Design",
  },
  {
    name: "Jordan Lee",
    role: "Full-Stack Dev",
    bio: "Building robust backends and sleek frontends with a passion for code.",
    avatar:
      "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
    badge: "Engineering",
  },
  {
    name: "Taylor Kim",
    role: "Product Manager",
    bio: "Bridging visions to reality, one feature at a time.",
    avatar:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
    badge: "Product",
  },
  {
    name: "Casey Patel",
    role: "UX Researcher",
    bio: "Uncovering user needs to shape intuitive journeys.",
    avatar:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
    badge: "Research",
  },
]

const leftColumn = teamMembers.slice(0, teamMembers.length / 2)
const rightColumn = teamMembers.slice(teamMembers.length / 2)

const TeamCard = ({
  avatar,
  name,
  role,
  bio,
  badge,
}: {
  avatar: string
  name: string
  role: string
  bio: string
  badge: string
}) => {
  return (
    <Card className="h-full w-48 border-0 shadow-sm transition-shadow duration-300 hover:shadow-md">
      <CardHeader className="pb-2">
        <div className="flex items-center gap-3">
          <Avatar className="h-12 w-12">
            <AvatarImage src={avatar} alt={name} />
            <AvatarFallback className="bg-primary text-primary-foreground">
              {name.charAt(0)}
            </AvatarFallback>
          </Avatar>
          <div className="space-y-1">
            <CardTitle className="text-base leading-tight font-semibold">
              {name}
            </CardTitle>
            <Badge variant="outline" className="text-xs capitalize">
              {badge}
            </Badge>
          </div>
        </div>
      </CardHeader>
      <CardContent>
        <p className="text-muted-foreground text-xs leading-relaxed">{bio}</p>
        <p className="text-primary mt-2 text-xs font-medium">{role}</p>
      </CardContent>
    </Card>
  )
}

export function TeamMarqueeVerticalDemo() {
  return (
    <div className="relative flex h-[500px] w-full flex-row items-center justify-center overflow-hidden">
      <Marquee vertical pauseOnHover className="[--duration:20s]">
        {leftColumn.map((member, index) => (
          <TeamCard key={index} {...member} />
        ))}
      </Marquee>
      <Marquee vertical reverse pauseOnHover className="[--duration:20s]">
        {rightColumn.map((member, index) => (
          <TeamCard key={index} {...member} />
        ))}
      </Marquee>
    </div>
  )
}

3D

import { cn } from "@/lib/utils"
import { Marquee } from "@/components/ui/marquee"

const teamMembers = [
  {
    name: "Alex Rivera",
    role: "Lead Designer",
    bio: "Crafting pixel-perfect experiences that delight users worldwide.",
    avatar:
      "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
  },
  {
    name: "Jordan Lee",
    role: "Full-Stack Dev",
    bio: "Building robust backends and sleek frontends with a passion for code.",
    avatar:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
  },
  {
    name: "Taylor Kim",
    role: "Product Manager",
    bio: "Bridging visions to reality, one feature at a time.",
    avatar:
      "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
  },
  {
    name: "Casey Patel",
    role: "UX Researcher",
    bio: "Uncovering user needs to shape intuitive journeys.",
    avatar:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
  },
]

const firstRow = teamMembers.slice(0, teamMembers.length / 2)
const secondRow = teamMembers.slice(teamMembers.length / 2)
const thirdRow = teamMembers.slice(0, teamMembers.length / 2)
const fourthRow = teamMembers.slice(teamMembers.length / 2)

const TeamCard = ({
  avatar,
  name,
  role,
  bio,
}: {
  avatar: string
  name: string
  role: string
  bio: string
}) => {
  return (
    <figure
      className={cn(
        "relative h-full w-fit cursor-pointer overflow-hidden rounded-xl border p-4 sm:w-36",
        // light styles
        "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]",
        // dark styles
        "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
      )}
    >
      <div className="flex flex-row items-center gap-2">
        <img
          className="rounded-full"
          width="32"
          height="32"
          alt=""
          src={avatar}
        />
        <div className="flex flex-col">
          <figcaption className="text-sm font-medium dark:text-white">
            {name}
          </figcaption>
          <p className="text-xs font-medium dark:text-white/40">{role}</p>
        </div>
      </div>
      <blockquote className="mt-2 text-sm">{bio}</blockquote>
    </figure>
  )
}

export function Marquee3D() {
  return (
    <div className="relative flex h-96 w-full flex-row items-center justify-center gap-4 overflow-hidden [perspective:300px]">
      <div
        className="flex flex-row items-center gap-4"
        style={{
          transform:
            "translateX(0px) translateY(-50px) translateZ(-150px) rotateX(15deg) rotateY(0deg) rotateZ(0deg)",
        }}
      >
        <Marquee pauseOnHover vertical className="[--duration:20s]">
          {firstRow.map((review) => (
            <TeamCard key={review.role} {...review} />
          ))}
        </Marquee>
        <Marquee reverse pauseOnHover className="[--duration:20s]" vertical>
          {secondRow.map((review) => (
            <TeamCard key={review.role} {...review} />
          ))}
        </Marquee>
        <Marquee reverse pauseOnHover className="[--duration:20s]" vertical>
          {thirdRow.map((review) => (
            <TeamCard key={review.role} {...review} />
          ))}
        </Marquee>
        <Marquee pauseOnHover className="[--duration:20s]" vertical>
          {fourthRow.map((review) => (
            <TeamCard key={review.role} {...review} />
          ))}
        </Marquee>
      </div>

      <div className="from-background pointer-events-none absolute inset-x-0 top-0 h-1/4 bg-gradient-to-b"></div>
      <div className="from-background pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t"></div>
      <div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div>
      <div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div>
    </div>
  )
}

Usage

import { Marquee } from "@/components/ui/marquee"
<Marquee>
  <span>Next.js</span>
  <span>React</span>
  <span>TypeScript</span>
  <span>Tailwind CSS</span>
</Marquee>

Props

PropTypeDefaultDescription
classNamestring-The class name to apply to the component.
reversebooleanfalseWhether or not to reverse the direction of the marquee.
pauseOnHoverbooleanfalseWhether or not to pause the marquee when the user hovers over the component.
verticalbooleanfalseWhether or not to display the marquee vertically.
childrennode-The content to display in the marquee.
repeatnumber4The number of times to repeat the content.