Logo Ticker Infinite
blockmarketingInfinite marquee brand partner ticker with sector category filters, speed and pause controls, and interactive telemetry hover cards.
Installation
$pnpm dlx uipkge-ng@latest add logo-ticker-infinite$npx uipkge-ng@latest add logo-ticker-infinite$yarn dlx uipkge-ng@latest add logo-ticker-infinite$bunx uipkge-ng@latest add logo-ticker-infiniteManifest:
https://uipkge.dev/r/angular/logo-ticker-infinite.jsonInstalls to:src/app/components/blocks/logo-ticker-infinite/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TickerBrandPartnerinterface TickerBrandPartner {
id: string
name: string
category: 'devtools' | 'fintech' | 'enterprise'
role: string
metric: string
region: string
}npm dependencies
Includes
Files installed (2)
src/app/components/blocks/logo-ticker-infinite/logo-ticker-infinite.component.ts10.2 kB
// Infinite marquee brand partner ticker with sector category filters, speed and // pause controls, and interactive telemetry hover cards. Port of the Vue/React // block 1:1 — the track repeats the filtered list twice for a seamless loop; // hover a tile to inspect its live integration telemetry. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { LucideAngularModule, Pause, Play } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' export interface TickerBrandPartner { id: string name: string category: 'devtools' | 'fintech' | 'enterprise' role: string metric: string region: string } export type LogoTickerCategory = 'all' | 'devtools' | 'fintech' | 'enterprise' const partners: TickerBrandPartner[] = [ { id: 'vercel', name: 'Vercel Edge', category: 'devtools', role: 'Global Edge Ingestion', metric: '99.95% SLA', region: 'Global', }, { id: 'supabase', name: 'Supabase Postgres', category: 'devtools', role: 'Real-Time Replication', metric: '1.2M QPS', region: 'US-East', }, { id: 'linear', name: 'Linear Method', category: 'enterprise', role: 'Issue Sync Engine', metric: '< 20ms Sync', region: 'Global', }, { id: 'stripe', name: 'Stripe Billing', category: 'fintech', role: 'Usage Metering Gateway', metric: '$120M+ Processed', region: 'Global', }, { id: 'resend', name: 'Resend SMTP', category: 'devtools', role: 'Transactional Dispatch', metric: '200M msgs/mo', region: 'US-West', }, { id: 'cloudflare', name: 'Cloudflare Workers', category: 'devtools', role: 'Zero-Trust Isolation', metric: '300+ Cities', region: 'Global', }, { id: 'planetscale', name: 'PlanetScale DB', category: 'enterprise', role: 'Branching Schema', metric: 'Zero Downtime', region: 'EU-Central', }, { id: 'brex', name: 'Brex Treasury', category: 'fintech', role: 'Ledger Reconciliation', metric: 'Real-time API', region: 'US-East', }, ] const categoryFilters: LogoTickerCategory[] = ['all', 'devtools', 'fintech', 'enterprise'] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-logo-ticker-infinite, [ui-logo-ticker-infinite]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent], styles: [ ` @keyframes ui-logo-ticker-marquee { 0% { transform: translateX(0%); } 100% { transform: translateX(-50%); } } .animate-marquee { animation: ui-logo-ticker-marquee 32s linear infinite; } `, ], template: ` <section data-slot="logo-ticker-infinite" [class]="rootClass"> <div class="mx-auto max-w-7xl space-y-8 text-center"> <!-- Section Header & Category Filter Controls --> <div class="border-border/60 flex flex-col items-center justify-between gap-4 border-b pb-6 text-left md:flex-row" > <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="bg-success size-2 rounded-full"></span> <p class="text-foreground font-mono text-xs font-semibold tracking-wider uppercase"> Production Verified Infrastructure </p> </div> <p class="text-muted-foreground text-xs"> Trusted by modern engineering teams shipping high-throughput production workloads. </p> </div> <!-- Filter & Control Buttons --> <div class="flex flex-wrap items-center gap-2"> <!-- Category Filter Pills --> <div class="bg-background border-border flex items-center gap-1 rounded-lg border p-0.5"> @for (cat of filters; track cat) { <button type="button" [class]="filterPillClass(cat)" (click)="setCategory(cat)"> {{ cat }} </button> } </div> <!-- Marquee Speed / Pause Controls --> <div class="flex items-center gap-1"> <button type="button" class="border-border bg-background text-muted-foreground hover:text-foreground rounded-lg border p-1.5 transition-colors" [attr.title]="isPaused() ? 'Resume Ticker' : 'Pause Ticker'" [attr.aria-label]="isPaused() ? 'Resume Ticker' : 'Pause Ticker'" (click)="togglePaused()" > @if (isPaused()) { <lucide-icon [img]="Play" class="size-3.5 fill-current" /> } @else { <lucide-icon [img]="Pause" class="size-3.5" /> } </button> <button type="button" class="border-border bg-background text-muted-foreground hover:text-foreground rounded-lg border px-2 py-1 font-mono text-xs transition-colors" [attr.aria-label]="isFastSpeed() ? 'Normal speed' : 'Double speed'" (click)="toggleSpeed()" > {{ isFastSpeed() ? '2x' : '1x' }} </button> </div> </div> </div> <!-- Infinite Marquee Track Viewport with Gradient Fade Edges --> <div class="group relative overflow-hidden"> <!-- Left & Right Gradient Shadows --> <div class="from-background via-background/80 pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r to-transparent sm:w-28" ></div> <div class="from-background via-background/80 pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l to-transparent sm:w-28" ></div> <!-- Double Track Container for Seamless Infinite Scrolling --> <div [class]="trackClass" [style.animation-play-state]="isPaused() ? 'paused' : 'running'"> <!-- Repeat partners array twice for infinite seamless loop --> @for (item of loopedPartners(); track item.id + '-' + item.index) { <div class="border-border bg-card/80 hover:bg-card hover:border-primary/50 relative flex shrink-0 cursor-pointer items-center gap-3 rounded-xl border px-4 py-2.5 shadow-xs backdrop-blur-md transition-colors duration-200" (mouseenter)="setHovered(item.partner)" (mouseleave)="clearHovered()" > <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-lg font-mono text-xs font-bold" > {{ item.partner.name.charAt(0) }} </div> <div class="text-left"> <p class="text-foreground font-mono text-xs font-semibold tracking-tight">{{ item.partner.name }}</p> <p class="text-muted-foreground text-xs">{{ item.partner.role }}</p> </div> <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success ml-1 font-mono text-xs" > {{ item.partner.metric }} </span> </div> } </div> </div> <!-- Live Hover Telemetry Card --> @if (hoveredBrand(); as hovered) { <div class="border-border bg-card animate-in fade-in-0 mx-auto max-w-md rounded-xl border p-3 text-left shadow-lg duration-150" > <div class="flex items-center justify-between font-mono text-xs"> <span class="text-foreground font-bold">{{ hovered.name }} Architecture Integration</span> <span class="text-success">{{ hovered.region }}</span> </div> <p class="text-muted-foreground mt-1 text-xs"> Connected through {{ hovered.role }} • Monitored with {{ hovered.metric }} verified telemetry. </p> </div> } </div> </section> `, }) export class UiLogoTickerInfiniteComponent { protected readonly Pause = Pause protected readonly Play = Play protected readonly filters = categoryFilters @Input('class') className?: string @Input() set initialCategory(v: LogoTickerCategory | undefined) { if (v) this.selectedCategory.set(v) } readonly selectedCategory = signal<LogoTickerCategory>('all') readonly isPaused = signal(false) readonly isFastSpeed = signal(false) readonly hoveredBrand = signal<TickerBrandPartner | null>(null) readonly filteredPartners = computed(() => { const selected = this.selectedCategory() if (selected === 'all') return partners return partners.filter((p) => p.category === selected) }) /** The filtered list twice, so the -50% marquee loop is seamless. */ readonly loopedPartners = computed(() => { const list = this.filteredPartners() return [...list, ...list].map((partner, index) => ({ partner, index })) }) get rootClass(): string { return cn( 'border-border/80 bg-muted/20 relative overflow-hidden border-y px-4 py-14 sm:px-6 sm:py-20 lg:px-8', this.className, ) } get trackClass(): string { return cn( 'flex w-max items-center gap-4 py-3', !this.isPaused() && 'animate-marquee', this.isFastSpeed() ? '[animation-duration:15s]' : '[animation-duration:32s]', ) } filterPillClass(cat: LogoTickerCategory): string { return cn( 'rounded-md px-2.5 py-1 font-mono text-xs capitalize transition-colors', this.selectedCategory() === cat ? 'bg-primary text-primary-foreground font-semibold shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } setCategory(cat: LogoTickerCategory): void { this.selectedCategory.set(cat) } togglePaused(): void { this.isPaused.update((v) => !v) } toggleSpeed(): void { this.isFastSpeed.update((v) => !v) } setHovered(partner: TickerBrandPartner): void { this.hoveredBrand.set(partner) } clearHovered(): void { this.hoveredBrand.set(null) } }src/app/components/blocks/logo-ticker-infinite/index.ts0.1 kB
export { UiLogoTickerInfiniteComponent, type TickerBrandPartner, type LogoTickerCategory, } from './logo-ticker-infinite.component'
Raw manifest:https://uipkge.dev/r/angular/logo-ticker-infinite.json