Marquee
marqueeuiContinuously 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
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/marquee.json$npx shadcn@latest add https://uipkge.dev/r/lit/marquee.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/marquee.json$bunx shadcn@latest add https://uipkge.dev/r/lit/marquee.jsonManifest:
https://uipkge.dev/r/lit/marquee.jsonInstalls to:src/uip/components/marquee/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
orientation | MarqueeOrientation | horizontal | optional |
direction | MarqueeDirection | left | optional |
speed | number | 20 | optional |
pause-on-hover | boolean | false | optional |
gap | number | 16 | optional |
repeat | number | 2 | optional |
paused | boolean | false | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-marquee': UipMarquee
}
}Files installed (1)
src/uip/components/marquee/marquee.ts4.4 kB
import { LitElement, css, html, isServer } from 'lit' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' const marqueeStyles = css` @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; } } ` export type MarqueeOrientation = 'horizontal' | 'vertical' export type MarqueeDirection = 'left' | 'right' | 'up' | 'down' /** * <uip-marquee> — infinitely scrolling marquee banner. */ export class UipMarquee extends LitElement { static styles = [ tailwind, marqueeStyles, css` :host { display: block; overflow: hidden; } `, ] static properties = { orientation: { reflect: true }, direction: { reflect: true }, speed: { type: Number, reflect: true }, pauseOnHover: { type: Boolean, attribute: 'pause-on-hover', reflect: true }, gap: { type: Number, reflect: true }, repeat: { type: Number, reflect: true }, paused: { type: Boolean, reflect: true }, } orientation: MarqueeOrientation = 'horizontal' direction: MarqueeDirection = 'left' speed = 20 pauseOnHover = false gap = 16 repeat = 2 paused = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'marquee') this.setAttribute('role', 'region') this.setAttribute('aria-roledescription', 'marquee') } willUpdate() { this.setAttribute('data-orientation', this.orientation) this.setAttribute('data-direction', this.direction) } protected updated(changedProps: Map<string, unknown>) { super.updated(changedProps) if (changedProps.has('repeat')) { this.cloneNodes() } } private onSlotChange(e: Event) { const slot = e.target as HTMLSlotElement const assigned = slot.assignedNodes({ flatten: true }) this.cloneNodes(assigned) } private cloneNodes(nodes?: Node[]) { if (isServer) return if (!nodes) { const slot = this.renderRoot.querySelector<HTMLSlotElement>('slot') if (!slot) return nodes = slot.assignedNodes({ flatten: true }) } const cloneContainers = this.renderRoot.querySelectorAll<HTMLElement>('[data-clone]') cloneContainers.forEach((container) => { container.replaceChildren(...nodes!.map((n) => n.cloneNode(true))) }) } render() { const isVertical = this.orientation === 'vertical' const reverse = this.direction === 'right' || this.direction === 'down' const containerClass = cn( 'group flex overflow-hidden', isVertical ? 'flex-col' : 'flex-row', this.pauseOnHover && 'hover:[&>[data-slot=marquee-track]]:[animation-play-state:paused]', ) const trackClass = cn( 'flex shrink-0', isVertical ? 'flex-col' : 'flex-row', this.paused && '![animation-play-state:paused]', ) const trackStyle = `gap: var(--marquee-gap); animation-name: ${ isVertical ? 'uipkge-marquee-y' : 'uipkge-marquee-x' }; animation-duration: ${this.speed}s; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: ${ reverse ? 'reverse' : 'normal' };` return html` <div part="base" data-slot="marquee" data-orientation=${this.orientation} data-direction=${this.direction} class=${containerClass} style="--marquee-gap: ${this.gap}px;" > <div data-slot="marquee-track" class=${trackClass} style=${trackStyle}> <slot @slotchange=${this.onSlotChange}></slot> </div> ${Array.from({ length: Math.max(0, this.repeat - 1) }, (_, i) => html` <div data-slot="marquee-track" data-clone=${i + 1} class=${trackClass} style=${trackStyle} aria-hidden="true" ></div> `)} </div> ` } } customElements.get('uip-marquee') || customElements.define('uip-marquee', UipMarquee) declare global { interface HTMLElementTagNameMap { 'uip-marquee': UipMarquee } }
Raw manifest:https://uipkge.dev/r/lit/marquee.json