Seamless Logo Marquee
An infinite horizontal logo scroll with masked edges, a pause-on-hover affordance and a reduced-motion fallback that wraps instead of moving.
Customer logos under segment headings as a description list, with an “and 40 more” count for the ones under NDA — answers whether you have done this for someone like the reader, which a flat wall cannot.
components/logo-segments.tsxnpx hoverlab add logo-segments
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Customers
Named with permission. Where a segment shows fewer logos than customers, the rest are under agreements that do not allow it.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <LogoSegments> — customer logos grouped by the industry they are in.
*
* An undifferentiated logo wall answers "are you real". It does not answer
* the question an enterprise buyer is actually asking, which is "have you
* done this for someone like me" — and on an ungrouped wall they answer it
* by scanning twenty names for one they recognise from their own sector, and
* usually giving up. Two names under a heading that says `Healthcare` does
* more work than twenty names under no heading at all.
*
* It is also the honest shape for an uneven customer base. Most companies
* are strong in one vertical and thin in two others, and a flat wall hides
* that in a way the first sales call immediately un-hides. Showing four
* logos under one segment and two under another is a claim that survives the
* meeting, and it tells a buyer in the thin segment that they would be
* early — which for some of them is the reason to buy.
*
* Structured as a <dl>: each segment name is a term and its logos are its
* description, so the grouping is real to a screen reader rather than being
* a visual accident of where the headings sit. A wall of <ul>s under <h3>s
* would look identical and carry none of that.
*
* `count` is separate from the logos on purpose. A segment usually has more
* customers than it has logos cleared for use, and "and 40 more" is both
* true and stronger than silently showing six.
*
* Text wordmarks by default, matching <LogoGrid> and <LogoStrip>.
*/
import * as React from 'react'
export interface LogoSegment {
name: string
/** Wordmarks, or any node — an <img>, an inline <svg>. */
logos: React.ReactNode[]
/** e.g. "and 40 more" — for customers who exist but cannot be named. */
count?: string
}
export interface LogoSegmentsProps {
eyebrow?: string
heading?: string
subheading?: string
segments?: LogoSegment[]
className?: string
}
const DEFAULT_SEGMENTS: LogoSegment[] = [
{
name: 'Financial services',
logos: ['Fieldstone', 'Meridian Capital', 'Umbra', 'Halden Trust'],
count: 'and 40 more',
},
{
name: 'Healthcare',
logos: ['Alcove Health', 'Coastline', 'Lumon Care'],
count: 'and 18 more',
},
{
name: 'Logistics',
logos: ['Northwind Freight', 'Vandelay', 'Portside'],
count: 'and 12 more',
},
{
name: 'Public sector',
logos: ['City of Ravel', 'Kestrel Authority'],
},
]
/*
A stable id suffix derived from this instance's own heading.
`useId` is the right answer and is not available here: this is a server
component and hooks are not. The heading is what differs when a section is
used twice on one page, so hashing it gives each copy its own
`aria-labelledby` target without a hook, a prop or a counter — and it
stays stable across server and client renders, which a counter would not.
*/
function instanceId(...parts: (string | undefined)[]): string {
const text = parts.filter(Boolean).join('|')
let hash = 0
for (let i = 0; i < text.length; i++) hash = (Math.imul(hash, 31) + text.charCodeAt(i)) | 0
return (hash >>> 0).toString(36).slice(0, 6)
}
export function LogoSegments({
eyebrow = 'Customers',
heading = 'Someone in your industry has already done this',
subheading = 'Named with permission. Where a segment shows fewer logos than customers, the rest are under agreements that do not allow it.',
segments = DEFAULT_SEGMENTS,
className = '',
}: LogoSegmentsProps) {
const headingId = `logo-segments-heading-${instanceId(heading, eyebrow)}`
return (
<section
aria-labelledby={headingId}
className={`mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
>
<div className="max-w-2xl">
<p className="text-sm font-medium uppercase tracking-wider text-primary">{eyebrow}</p>
<h2
id={headingId}
className="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl"
>
{heading}
</h2>
<p className="mt-4 text-pretty leading-relaxed text-muted-foreground">{subheading}</p>
</div>
<dl className="mt-12 divide-y divide-border/60 border-y border-border/60">
{segments.map((segment) => (
<div
key={segment.name}
className="grid gap-4 py-8 sm:grid-cols-[minmax(0,min(12rem,38%))_minmax(0,1fr)] sm:gap-10"
>
<dt className="min-w-0 break-words text-sm font-semibold tracking-tight text-foreground">
{segment.name}
{segment.count ? (
<span className="mt-1 block text-sm font-normal text-muted-foreground">
{segment.count}
</span>
) : null}
</dt>
<dd className="min-w-0">
<ul className="flex flex-wrap items-center gap-x-10 gap-y-4">
{segment.logos.map((logo, i) => (
<li
key={i}
className="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground"
>
{logo}
</li>
))}
</ul>
</dd>
</div>
))}
</dl>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/logo-segments.tsx and import it where you need the section:
import { LogoSegments } from '@/components/logo-segments'2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
eyebrow | string | 'Customers' |
heading | string | 'Someone in your industry has already done this' |
subheading | string | — |
segments | LogoSegment[] | DEFAULT_SEGMENTS |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
<!--
Logos Grouped by Industry — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block has no interactive behaviour, so nothing is missing.
-->
<section aria-labelledby="logo-segments-heading-jb4bb9" class="mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 lg:px-8 ">
<div class="max-w-2xl">
<p class="text-sm font-medium uppercase tracking-wider text-primary">Customers</p>
<h2 id="logo-segments-heading-jb4bb9" class="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl">Someone in your industry has already done this</h2>
<p class="mt-4 text-pretty leading-relaxed text-muted-foreground">Named with permission. Where a segment shows fewer logos than customers, the rest are under agreements that do not allow it.</p>
</div>
<dl class="mt-12 divide-y divide-border/60 border-y border-border/60">
<div class="grid gap-4 py-8 sm:grid-cols-[minmax(0,min(12rem,38%))_minmax(0,1fr)] sm:gap-10">
<dt class="min-w-0 break-words text-sm font-semibold tracking-tight text-foreground">Financial services<span class="mt-1 block text-sm font-normal text-muted-foreground">and 40 more</span></dt>
<dd class="min-w-0">
<ul class="flex flex-wrap items-center gap-x-10 gap-y-4">
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Fieldstone</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Meridian Capital</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Umbra</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Halden Trust</li>
</ul>
</dd>
</div>
<div class="grid gap-4 py-8 sm:grid-cols-[minmax(0,min(12rem,38%))_minmax(0,1fr)] sm:gap-10">
<dt class="min-w-0 break-words text-sm font-semibold tracking-tight text-foreground">Healthcare<span class="mt-1 block text-sm font-normal text-muted-foreground">and 18 more</span></dt>
<dd class="min-w-0">
<ul class="flex flex-wrap items-center gap-x-10 gap-y-4">
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Alcove Health</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Coastline</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Lumon Care</li>
</ul>
</dd>
</div>
<div class="grid gap-4 py-8 sm:grid-cols-[minmax(0,min(12rem,38%))_minmax(0,1fr)] sm:gap-10">
<dt class="min-w-0 break-words text-sm font-semibold tracking-tight text-foreground">Logistics<span class="mt-1 block text-sm font-normal text-muted-foreground">and 12 more</span></dt>
<dd class="min-w-0">
<ul class="flex flex-wrap items-center gap-x-10 gap-y-4">
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Northwind Freight</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Vandelay</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Portside</li>
</ul>
</dd>
</div>
<div class="grid gap-4 py-8 sm:grid-cols-[minmax(0,min(12rem,38%))_minmax(0,1fr)] sm:gap-10">
<dt class="min-w-0 break-words text-sm font-semibold tracking-tight text-foreground">Public sector</dt>
<dd class="min-w-0">
<ul class="flex flex-wrap items-center gap-x-10 gap-y-4">
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">City of Ravel</li>
<li class="text-lg font-bold tracking-tight text-muted-foreground/85 transition-colors hover:text-foreground">Kestrel Authority</li>
</ul>
</dd>
</div>
</dl>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Logo CloudsCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
An infinite horizontal logo scroll with masked edges, a pause-on-hover affordance and a reduced-motion fallback that wraps instead of moving.
Hairline-separated cells that hold still and show the whole set at once — the logo wall for a footer, a customers page, or anywhere the count is the claim.
One unbordered line of wordmarks beside a claim you can actually be wrong about, sized to sit under a hero without pushing the CTA down or moving while the headline is being read.