Menu

Change language

GitHub repo

Marquee

marqueeui

Continuously auto-scrolling content. Scrolls horizontally or vertically in any direction, with configurable speed, gap, repeat count for a seamless loop, pause-on-hover, and a hard pause prop. Respects prefers-reduced-motion.

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/marquee.json
Named registry:npx shadcn-vue@latest add @uipkge/marqueeInstalls to:app/components/ui/marquee/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
orientation

Scroll axis.

'horizontal''vertical'
'horizontal'optional
direction

Travel direction.

'left''right''up''down'
'left'optional
speed

Animation duration in seconds. Lower = faster.

number20optional
pauseOnHover

Pause the animation on hover.

booleanfalseoptional
gap

Gap between repeated content groups (px).

number16optional
repeat

Number of times the slot content is duplicated for a seamless loop.

number2optional
paused

Hard pause the animation.

booleanfalseoptional
classHTMLAttributes['class']—optional

Files installed (2)

  • app/components/ui/marquee/Marquee.vue2.8 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = withDefaults(
      defineProps<{
        /** Scroll axis. */
        orientation?: 'horizontal' | 'vertical'
        /** Travel direction. */
        direction?: 'left' | 'right' | 'up' | 'down'
        /** Animation duration in seconds. Lower = faster. */
        speed?: number
        /** Pause the animation on hover. */
        pauseOnHover?: boolean
        /** Gap between repeated content groups (px). */
        gap?: number
        /** Number of times the slot content is duplicated for a seamless loop. */
        repeat?: number
        /** Hard pause the animation. */
        paused?: boolean
        class?: HTMLAttributes['class']
      }>(),
      {
        orientation: 'horizontal',
        direction: 'left',
        speed: 20,
        pauseOnHover: false,
        gap: 16,
        repeat: 2,
        paused: false,
      },
    )
    
    const isVertical = computed(() => props.orientation === 'vertical')
    const reverse = computed(() => props.direction === 'right' || props.direction === 'down')
    
    const durationStyle = computed(() => `${props.speed}s`)
    const gapStyle = computed(() => `${props.gap}px`)
    
    const containerClass = computed(() =>
      cn(
        'group flex overflow-hidden',
        isVertical.value ? 'flex-col' : 'flex-row',
        props.pauseOnHover ? 'hover:[&>[data-slot=marquee-track]]:[animation-play-state:paused]' : '',
        props.class,
      ),
    )
    
    const trackClass = computed(() =>
      cn('flex shrink-0', isVertical.value ? 'flex-col' : 'flex-row', props.paused ? '![animation-play-state:paused]' : ''),
    )
    </script>
    
    <template>
      <div
        data-uipkge
        data-slot="marquee"
        :data-orientation="orientation"
        :data-direction="direction"
        :class="containerClass"
        :style="{
          '--marquee-gap': gapStyle,
        }"
        role="region"
        aria-roledescription="marquee"
      >
        <div
          v-for="i in repeat"
          :key="i"
          data-slot="marquee-track"
          :class="trackClass"
          :style="{
            gap: 'var(--marquee-gap)',
            animationName: isVertical ? 'uipkge-marquee-y' : 'uipkge-marquee-x',
            animationDuration: `${props.speed}s`,
            animationTimingFunction: 'linear',
            animationIterationCount: 'infinite',
            animationDirection: reverse ? 'reverse' : 'normal',
          }"
          :aria-hidden="i > 1 ? 'true' : undefined"
        >
          <slot />
        </div>
      </div>
    </template>
    
    <style>
    /* Global keyframes — must NOT be scoped so the inline animationName can find them */
    @keyframes uipkge-marquee-x {
      from {
        transform: translateX(0);
      }
      to {
        transform: translateX(-100%);
      }
    }
    
    @keyframes uipkge-marquee-y {
      from {
        transform: translateY(0);
      }
      to {
        transform: translateY(-100%);
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      [data-slot='marquee-track'] {
        animation: none !important;
      }
    }
    </style>
    
  • app/components/ui/marquee/index.ts0 kB

Raw manifest:https://uipkge.dev/r/vue/marquee.json