UIPackage
Menu

Marquee

marquee ui
Edit on GitHub

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.

Also available for React ->

Installation

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

Examples

Props

Name Type / Values Default Required
orientation

Scroll axis.

'horizontal''vertical'
'horizontal' optional
direction

Travel direction.

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

Animation duration in seconds. Lower = faster.

number 20 optional
pauseOnHover

Pause the animation on hover.

boolean false optional
gap

Gap between repeated content groups (px).

number 16 optional
repeat

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

number 2 optional
paused

Hard pause the animation.

boolean false optional
class HTMLAttributes['class'] optional

Files installed (2)

  • app/components/ui/marquee/Marquee.vue 2.7 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,
        }"
      >
        <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.ts 0 kB
    export { default as Marquee } from './Marquee.vue'

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