Logo links marquee
BlockA heading over an endless row of large, linked logos that pop in one after another and tilt on hover, with a pause control.
Installation
The block is written to your blocks directory, and everything it is built from is installed alongside it.
pnpm dlx @dowel-ui/cli add logo-links-marqueeBuilt from
A block is not a black box. Every piece is a component you already have documentation for, and can change independently.
Props
LogoLinksMarqueeBlock
| Prop | Type | Default |
|---|---|---|
description | ReactNode | "Join thousands of developers and designers already building with us." |
heading | ReactNode | "Trusted by the world's most innovative teams" |
headingLevelLevel of the section heading. | 1 | 2 | 3 | 4 | 5 | 6 | 2 |
labelsText of the pause control in each state. | Partial<LogoLinksMarqueeLabels> | — |
logos | LogoLinksMarqueeLogo[] | DEFAULT_MARQUEE_LINKS |
speedScroll speed in pixels per second. | number | 40 |
Plus every attribute of <section> except title.
Accessibility
One section landmark named by its heading (level set with `headingLevel`). The row is Marquee, so each logo link exists once for assistive technology and the keyboard — the copies that close the loop are aria-hidden and inert — and it pauses on hover and focus. A visible Pause button stops it outright (WCAG 2.2.2); under reduced motion the row stops and becomes a scrollable region, and the button is hidden because there is nothing to pause. Each entry is named by its organisation; the graphic is aria-hidden.
Source
This is exactly what dowel add logo-links-marquee writes, with imports rewritten to your own path alias. It is a starting point — edit it.
"use client";
// Ported from SmoothUI Logo Cloud 2 (MIT, © 2024 Eduardo Calvo). See THIRD_PARTY_NOTICES.md.
import {
useId,
useState,
type ComponentPropsWithRef,
type CSSProperties,
type ReactNode,
} from "react";
import { Button } from "@/components/button";
import { Marquee } from "@/components/marquee";
import { focusRing } from "@/lib/styles";
import { cn } from "@/lib/utils";
/*
* A heading over an endless row of large, linked logos that pop in one after
* another and tilt when hovered.
*
* The loop is Dowel's Marquee: the source renders the row three times and
* animates it from JavaScript, and its copies are all focusable links, so a
* keyboard user tabs through every logo three times. Marquee renders the
* links once for assistive technology, pauses on hover and focus, and stops
* under reduced motion. Anything that moves for more than five seconds needs a
* way to stop it (WCAG 2.2.2), so the block adds a visible pause button.
*
* The entrances are @starting-style transitions: CSS only, never hidden
* without JavaScript, and instant under reduced motion.
*/
const PREFIX = "dowel-logo-links-marquee";
const STYLES = `
[data-slot=logo-links-marquee-header],[data-slot=logo-links-marquee-item]{transition:opacity var(--duration-slower) var(--ease-out-quint),translate var(--duration-slower) var(--ease-out-quint),scale var(--duration-slower) var(--ease-out-quint);transition-delay:calc(var(--${PREFIX}-index,0) * 100ms * var(--motion-scale,1))}
@starting-style{[data-slot=logo-links-marquee-header]{opacity:0;translate:0 20px}[data-slot=logo-links-marquee-item]{opacity:0;scale:.8}}
`;
export interface LogoLinksMarqueeLogo {
/** The organisation's name — the accessible name of the entry. */
name: string;
/** An SVG or image. Decorative: the name is announced instead. */
logo?: ReactNode;
/** Makes the entry a link. */
href?: string;
}
export const DEFAULT_MARQUEE_LINKS: LogoLinksMarqueeLogo[] = [
{ name: "Lumen" },
{ name: "Halcyon" },
{ name: "Meridian" },
{ name: "Tessera" },
{ name: "Quarry" },
{ name: "Oakline" },
{ name: "Brightwell" },
{ name: "Solace" },
];
export interface LogoLinksMarqueeLabels {
pause: string;
play: string;
}
export interface LogoLinksMarqueeBlockProps extends Omit<
ComponentPropsWithRef<"section">,
"title"
> {
heading?: ReactNode;
description?: ReactNode;
logos?: LogoLinksMarqueeLogo[];
/** Level of the section heading. */
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
/** Scroll speed in pixels per second. */
speed?: number;
/** Text of the pause control in each state. */
labels?: Partial<LogoLinksMarqueeLabels>;
}
function Mark({ name, logo }: LogoLinksMarqueeLogo) {
return (
<span
data-slot="logo-links-marquee-mark"
className="inline-flex transition-[scale,rotate] duration-[var(--duration-normal)] ease-[var(--ease-overshoot)] group-hover:scale-110 group-hover:rotate-3"
>
{logo === undefined ? (
<span className="text-2xl font-semibold tracking-tight">{name}</span>
) : (
<>
<span className="sr-only">{name}</span>
<span aria-hidden="true" className="contents *:fill-current">
{logo}
</span>
</>
)}
</span>
);
}
/** A heading over an endless row of linked logos, with a pause control. */
export function LogoLinksMarqueeBlock({
heading = "Trusted by the world's most innovative teams",
description = "Join thousands of developers and designers already building with us.",
logos = DEFAULT_MARQUEE_LINKS,
headingLevel = 2,
speed = 40,
labels,
className,
...props
}: LogoLinksMarqueeBlockProps) {
const headingId = useId();
const [paused, setPaused] = useState(false);
const Heading = `h${String(headingLevel)}` as "h2";
const text = { pause: "Pause logos", play: "Play logos", ...labels };
return (
<section
aria-labelledby={headingId}
data-slot="logo-links-marquee"
className={cn(
"mx-auto w-full max-w-7xl overflow-hidden px-4 py-16 sm:px-6 md:py-20",
className,
)}
{...props}
>
<style href={PREFIX} precedence="dowel">
{STYLES}
</style>
<div data-slot="logo-links-marquee-header" className="mb-12 text-center md:mb-16">
<Heading
id={headingId}
className="mb-4 text-2xl font-bold tracking-tight text-balance lg:text-3xl"
>
{heading}
</Heading>
{description ? (
<p className="text-lg text-pretty text-muted-foreground">{description}</p>
) : null}
</div>
<Marquee
speed={speed}
paused={paused}
gap={32}
style={{
maskImage:
"linear-gradient(to right, transparent, var(--color-foreground) 20%, var(--color-foreground) 80%, transparent)",
}}
>
<ul className="flex items-center gap-8">
{logos.map((entry, index) => (
<li
key={entry.name}
data-slot="logo-links-marquee-item"
style={{ [`--${PREFIX}-index`]: index } as CSSProperties}
className="flex shrink-0 text-foreground"
>
{entry.href ? (
<a
href={entry.href}
className={cn("group inline-flex rounded-md p-6", focusRing)}
>
<Mark {...entry} />
</a>
) : (
<span className="group inline-flex p-6">
<Mark {...entry} />
</span>
)}
</li>
))}
</ul>
</Marquee>
<div className="mt-4 flex justify-center motion-reduce:hidden">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setPaused((value) => !value);
}}
>
{paused ? text.play : text.pause}
</Button>
</div>
</section>
);
}