Skip to content
Palette Default
Download ZIP

Default palette · 8.3 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_customer_logo_strip_01 · version 1.0.0 · Default palette
Using the PageSugar MCP server, fetch component cmp_customer_logo_strip_01 version 1.0.0 with variant "default", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Default
Version
1.0.0
Digest
Full digest
sha256-5076e8dd40b465107b8682ad4f009ac4c07bf9335d55e7276f7cd80b7f263b5f
CustomerLogoStrip.svelte Svelte · 13.8 KB Raw
<script module lang="ts">
	export interface CustomerLogo {
		/** Company name; it becomes the image's alt text and, when linked, the link's name. */
		name: string;
		/** Image URL. An SVG trimmed to the mark's edges sizes most accurately. */
		src: string;
		/** Intrinsic width of the image, in any unit that matches height. */
		width: number;
		/** Intrinsic height of the image. Only the ratio of width to height is used. */
		height: number;
		/** Optional destination, opened in the same tab. */
		href?: string;
		/** Optical nudge on top of the automatic sizing, e.g. 0.9 for a dense mark. Default 1. */
		scale?: number;
	}
</script>

<script lang="ts">
	import { onMount } from 'svelte';

	interface Props {
		/** Logos you have permission to display, in display order. */
		logos: CustomerLogo[];
		/** Short line introducing the row, e.g. "Trusted by teams at". It also names the list. */
		label?: string;
		/** Accessible name for the list when there is no visible label. */
		listLabel?: string;
		/** 'static' wraps into rows; 'marquee' scrolls slowly and can be paused. */
		motion?: 'static' | 'marquee';
		/** 'original' shows each logo as supplied; 'mono' greys them through --customer-logo-strip-filter. */
		tone?: 'original' | 'mono';
		/** 'center' clusters the row under a centred label; 'between' spreads it edge to edge. */
		align?: 'center' | 'between';
		/** Accessible name of the marquee's pause button while it runs. */
		pauseLabel?: string;
		/** Accessible name of the same button while it is paused. */
		playLabel?: string;
	}

	let {
		logos,
		label,
		listLabel = 'Customers',
		motion = 'static',
		tone = 'original',
		align = 'center',
		pauseLabel = 'Pause scrolling logos',
		playLabel = 'Play scrolling logos'
	}: Props = $props();

	const uid = $props.id();

	/*
	 * Optical sizing. A fixed height makes a square monogram shout and a long wordmark whisper, so
	 * each logo's height is the base size times ratio^-0.4 (ratio = width / height), then the
	 * logo's own scale. Constant area would be ^-0.5, which starves wordmarks: the space between
	 * letters carries no weight. Clamped so no mark is taller than the base or under 40% of it.
	 */
	const factor = (logo: CustomerLogo) => {
		const ratio = logo.width > 0 && logo.height > 0 ? logo.width / logo.height : 1;
		const k = Math.pow(ratio, -0.4) * (logo.scale ?? 1);
		return Math.min(1, Math.max(0.4, k)).toFixed(3);
	};

	/*
	 * Below lg each logo takes an equal cell and rows centre, so no count leaves one logo alone
	 * on a row: phones run two a row (three past six logos, closing on two pairs when three
	 * would strand one), sm runs three or four, whichever divides the list better.
	 */
	const cell = (index: number) => {
		const n = logos.length;
		const phone = n <= 6 ? 'w-1/2' : n % 3 === 1 && index >= n - 4 ? 'w-1/2' : 'w-1/3';
		const small = n === 4 || (n > 6 && n % 4 !== 1) ? 'sm:w-1/4' : 'sm:w-1/3';
		return [phone, small];
	};

	/*
	 * From lg, at most six logos a row. Longer lists balance their rows: eight run four and four,
	 * sixteen run six, six and four, never a stranded pair.
	 */
	const perRow = $derived(
		logos.length <= 6 ? logos.length : Math.ceil(logos.length / Math.ceil(logos.length / 6))
	);
	/*
	 * 'between': logos start on the label's edge (the list pulls back the 12 px padding that
	 * gives each logo its target), and from lg auto-sized grid columns spread them edge to edge.
	 */
	const COLUMNS = [
		'',
		'lg:grid-cols-[repeat(1,minmax(0,auto))]',
		'lg:grid-cols-[repeat(2,minmax(0,auto))]',
		'lg:grid-cols-[repeat(3,minmax(0,auto))]',
		'lg:grid-cols-[repeat(4,minmax(0,auto))]',
		'lg:grid-cols-[repeat(5,minmax(0,auto))]',
		'lg:grid-cols-[repeat(6,minmax(0,auto))]'
	];
	/* 'center' past six logos: equal cells, so the last row centres under the others. */
	const WIDTHS = ['', '', '', '', 'lg:w-1/4', 'lg:w-1/5', 'lg:w-1/6'];

	/* A label long enough to wrap reads as a sentence: regular weight, and on phones it starts on the edge. */
	const longLabel = $derived((label?.length ?? 0) > 72);

	/*
	 * The marquee loops by sliding one copy of the list, so enough copies must lie end to end to
	 * cover the row plus one copy. The server estimates 120 px a logo; once mounted, the real
	 * widths are measured and followed on resize. Speed holds at 40 px a second.
	 */
	const SPEED = 40;
	let viewport = $state<HTMLElement>();
	let first = $state<HTMLElement>();
	let measured = $state<{ copies: number; seconds: number }>();
	$effect(() => {
		if (!viewport || !first || typeof ResizeObserver === 'undefined') return;
		const row = viewport;
		const copy = first;
		const measure = () => {
			const width = copy.getBoundingClientRect().width;
			if (width <= 0) return;
			measured = {
				copies: Math.max(2, Math.ceil(row.clientWidth / width) + 1),
				seconds: Math.round((width / SPEED) * 100) / 100
			};
		};
		const observer = new ResizeObserver(measure);
		observer.observe(row);
		observer.observe(copy);
		return () => observer.disconnect();
	});
	const copies = $derived(
		measured?.copies ?? Math.max(2, Math.ceil(1152 / Math.max(1, logos.length * 120)) + 1)
	);
	const duration = $derived(`${measured?.seconds ?? (logos.length * 120) / SPEED}s`);

	let paused = $state(false);
	/* Motion starts after hydration, so the pause button always works while anything moves. */
	let ready = $state(false);
	onMount(() => {
		ready = true;
	});
</script>

{#snippet mark(logo: CustomerLogo, index: number, hidden: boolean)}
	{#if logo.href}
		<a
			href={logo.href}
			tabindex={hidden ? -1 : undefined}
			class="item flex min-h-11 max-w-full min-w-0 items-center justify-center rounded-lg px-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)]"
		>
			<img
				src={logo.src}
				alt={hidden ? '' : logo.name}
				width={logo.width}
				height={logo.height}
				style:--_k={factor(logo)}
				class="mark w-auto max-w-none shrink-0"
				loading={index > 7 ? 'lazy' : undefined}
				decoding="async"
			/>
		</a>
	{:else}
		<span class="flex min-h-11 max-w-full min-w-0 items-center justify-center px-3">
			<img
				src={logo.src}
				alt={hidden ? '' : logo.name}
				width={logo.width}
				height={logo.height}
				style:--_k={factor(logo)}
				class="mark w-auto max-w-none shrink-0"
				loading={index > 7 ? 'lazy' : undefined}
				decoding="async"
			/>
		</span>
	{/if}
{/snippet}

{#snippet rows(fallback: boolean)}
	<ul
		class={[
			'static-row flex flex-wrap items-center gap-y-4 sm:gap-y-6',
			fallback && 'fallback',
			label && 'mt-6 sm:mt-8',
			align === 'between'
				? ['-mx-3 justify-start lg:grid lg:items-center lg:justify-between', COLUMNS[perRow]]
				: ['justify-center', logos.length <= 6 && 'lg:gap-x-12']
		]}
		role="list"
		aria-labelledby={label ? `${uid}-label` : undefined}
		aria-label={label ? undefined : listLabel}
	>
		{#each logos as logo, index (index)}
			<li
				class={[
					align === 'between'
						? 'flex min-w-0 justify-start lg:justify-center'
						: 'flex min-w-0 justify-center',
					cell(index),
					align === 'center' && logos.length > 6 ? WIDTHS[perRow] : 'lg:w-auto'
				]}
			>
				{@render mark(logo, index, false)}
			</li>
		{/each}
	</ul>
{/snippet}

{#if logos.length > 0}
	<section
		class={[
			'customer-logo-strip px-4 py-12 [--_size:2rem] sm:px-6 sm:py-16 sm:[--_size:2.25rem] lg:px-8 lg:py-20 lg:[--_size:2.5rem]',
			tone === 'mono' && 'tone-mono'
		]}
	>
		<div class="mx-auto max-w-6xl">
			{#if label}
				<p
					id="{uid}-label"
					class={[
						'text-sm/5 text-balance break-words text-[var(--_muted)]',
						longLabel ? 'font-normal' : 'font-medium',
						align === 'center'
							? ['mx-auto max-w-xl', longLabel ? 'text-start sm:text-center' : 'text-center']
							: 'max-w-xl text-start'
					]}
				>
					{label}
				</p>
			{/if}

			{#if motion === 'marquee'}
				<div
					class={['marquee flex items-center gap-4', label && 'mt-6 sm:mt-8']}
					data-ready={ready || undefined}
					data-paused={paused || undefined}
				>
					<div
						class="viewport min-w-0 flex-1 overflow-hidden"
						bind:this={viewport}
						onfocusin={(event) => {
							// Keyboard focus stops the row (see the CSS); bring a half-hidden logo fully into view.
							const target = event.target as HTMLElement;
							if (target.matches(':focus-visible'))
								target.scrollIntoView({ block: 'nearest', inline: 'nearest' });
						}}
						onfocusout={(event) => {
							// Tabbing may have scrolled the row to a later logo; hand the loop back its start.
							if (!viewport?.contains(event.relatedTarget as Node | null)) viewport!.scrollLeft = 0;
						}}
					>
						<div class="track flex w-max" style:--_copies={copies} style:--_duration={duration}>
							<ul
								bind:this={first}
								class="copy flex shrink-0 items-center gap-x-8 pe-8 sm:gap-x-12 sm:pe-12"
								role="list"
								aria-labelledby={label ? `${uid}-label` : undefined}
								aria-label={label ? undefined : listLabel}
							>
								{#each logos as logo, index (index)}
									<li class="shrink-0">{@render mark(logo, index, false)}</li>
								{/each}
							</ul>
							{#each { length: copies - 1 }, copy (copy)}
								<ul
									class="copy copy-hidden flex shrink-0 items-center gap-x-8 pe-8 sm:gap-x-12 sm:pe-12"
									role="list"
									aria-hidden="true"
								>
									{#each logos as logo, index (index)}
										<li class="shrink-0">{@render mark(logo, index, true)}</li>
									{/each}
								</ul>
							{/each}
						</div>
					</div>
					<button
						type="button"
						class="toggle flex size-8 shrink-0 items-center justify-center rounded-full text-[var(--_ink)] ring-1 ring-[var(--_hairline)] ring-inset focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--_accent)] pointer-coarse:size-11"
						aria-label={paused ? playLabel : pauseLabel}
						onclick={() => (paused = !paused)}
					>
						{#if paused}
							<svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
								<path
									d="M5 3.5v9a.5.5 0 0 0 .77.42l7-4.5a.5.5 0 0 0 0-.84l-7-4.5A.5.5 0 0 0 5 3.5Z"
								/>
							</svg>
						{:else}
							<svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
								<rect x="4" y="3" width="3" height="10" rx="1" />
								<rect x="9" y="3" width="3" height="10" rx="1" />
							</svg>
						{/if}
					</button>
				</div>
				<!-- Reduced motion shows the static rows instead; only one of the two lists is ever displayed. -->
				{@render rows(true)}
			{:else}
				{@render rows(false)}
			{/if}
		</div>
	</section>
{/if}

<style>
	/* Public tokens: set --customer-logo-strip-* on this section or any ancestor to retone it. */
	.customer-logo-strip {
		--_accent: var(--customer-logo-strip-accent, #18181b);
		--_on-accent: var(--customer-logo-strip-on-accent, #ffffff);
		--_ink: var(--customer-logo-strip-ink, #18181b);
		--_muted: var(--customer-logo-strip-muted, #52525b);
		--_hairline: var(--customer-logo-strip-hairline, rgb(0 0 0 / 0.1));
		--_filter: var(--customer-logo-strip-filter, grayscale(1) contrast(1.15) opacity(0.7));
	}

	/* Optical size: the base --_size times the logo's own factor from the script. */
	.mark {
		height: calc(var(--_size) * var(--_k));
	}

	/*
	 * Mono tone. A linked mark returns to its own colours on hover and focus, the one place the
	 * strip answers a pointer. Filter is transitioned alongside colour: it is the mark's tone.
	 */
	/* In static rows a mark never outgrows its cell; an over-wide wordmark scales down instead. */
	.static-row .mark {
		max-width: 100%;
		object-fit: contain;
	}

	.tone-mono .mark {
		filter: var(--_filter);
		transition: filter 150ms cubic-bezier(0.2, 0, 0, 1);
	}
	.tone-mono .item:is(:hover, :focus-visible) .mark {
		filter: none;
	}

	.item,
	.toggle {
		transition-property: background-color, transform;
		transition-duration: 150ms;
		transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
	}
	.item:hover,
	.toggle:hover {
		background-color: color-mix(in oklab, var(--_ink) 5%, transparent);
	}
	.toggle:active {
		transform: scale(0.96);
		transition-duration: 80ms;
	}

	/* Logos fade out at both ends of the track instead of being cut off. */
	.viewport {
		--_fade: 1.5rem;
		mask-image: linear-gradient(
			to right,
			transparent,
			#000 var(--_fade),
			#000 calc(100% - var(--_fade)),
			transparent
		);
	}
	@media (min-width: 40rem) {
		.viewport {
			--_fade: 3rem;
		}
	}

	/* Inside the clipped row the focus ring sits just inside the link, so nothing cuts it off. */
	.viewport .item:focus-visible {
		outline-offset: -2px;
	}

	/* Each copy ends with the same gap that separates its logos, so the seam is invisible. */
	/* Linear on purpose: a marquee moves at constant speed; eased loops pulse at every seam. */
	.marquee[data-ready] .track {
		animation: customer-logo-strip-scroll var(--_duration) linear infinite;
	}
	/*
	 * Pointer or focus on the logos pauses them; the button's own focus does not, or pressing
	 * Play would leave the row still until focus moved on.
	 */
	.viewport:is(:hover, :focus-within) .track,
	.marquee[data-paused] .track {
		animation-play-state: paused;
	}
	/*
	 * A keyboard user tabbing through linked logos gets the real row: no motion and no fades, so
	 * the browser can scroll each focused logo fully into view. It resumes when focus leaves.
	 */
	.viewport:has(:focus-visible) {
		mask-image: none;
	}
	.viewport:has(:focus-visible) .track {
		animation: none;
	}
	.track:dir(rtl) {
		--_direction: 1;
	}
	@keyframes customer-logo-strip-scroll {
		to {
			transform: translateX(calc(var(--_direction, -1) * 100% / var(--_copies)));
		}
	}

	/*
	 * The static rows stand in for the marquee under reduced motion. revert-layer hands display
	 * back to the Tailwind utilities (flex, or grid from lg for 'between').
	 */
	.fallback {
		display: none;
	}
	@media (prefers-reduced-motion: reduce) {
		.marquee {
			display: none;
		}
		.fallback {
			display: revert-layer;
		}
		.tone-mono .mark,
		.item,
		.toggle {
			transition: none;
		}
		.toggle:active {
			transform: none;
		}
	}
</style>

Renders the logos you pass, in order, and links the ones with an href in the same tab. The only state it keeps is the marquee's pause. It does not check that you have permission to show a company's logo, fetch logos, or ship any real ones; the preview marks are invented. The marquee needs client JavaScript to start and to pause; without it the row sits still and clipped at its first logos, so use motion="static" where scripts may not run.

Suggested location
src/lib/components/customer-logo-strip-01
Required props
logos

Limitations

  • Optical sizing uses only each logo's width and height. Artwork with generous padding inside its file looks small; trim the file or raise that logo's scale.
  • The mono tone is a CSS filter on the supplied image. Marks with light and dark parts keep their contrast; for a dark page, set --customer-logo-strip-filter (for example to brightness(0) invert(1) opacity(0.7)) or supply light versions of the logos.
  • The marquee measures its row and one copy of the list after mounting and lays out as many copies as the row needs. Before hydration (and during server rendering) it estimates 120 px a logo.
  • With an empty logos array nothing renders, not even the label.
  • Light appearance only. No dark mode is declared; the tokens retone it for a dark band.

Example

Svelte
<script lang="ts">
	import CustomerLogoStrip, {
		type CustomerLogo
	} from '$lib/components/customer-logo-strip-01/CustomerLogoStrip.svelte';

	// Only logos you have permission to display. width and height are the file's own proportions.
	const logos: CustomerLogo[] = [
		{ name: 'Fernhill', src: '/logos/fernhill.svg', width: 177, height: 37 },
		{ name: 'Ledgerly', src: '/logos/ledgerly.svg', width: 48, height: 48, href: '/customers/ledgerly' }
	];
</script>

<CustomerLogoStrip label="Trusted by teams at" {logos} tone="mono" />

Customer logo strip#

One row of customer, partner or stockist logos under a short label, usually straight after a hero. Pass only logos you have permission to show; the component cannot check that for you.

Svelte
<script lang="ts">
	import CustomerLogoStrip, {
		type CustomerLogo
	} from '$lib/components/customer-logo-strip-01/CustomerLogoStrip.svelte';

	const logos: CustomerLogo[] = [
		{ name: 'Fernhill', src: '/logos/fernhill.svg', width: 177, height: 37 },
		{ name: 'Oakline', src: '/logos/oakline.svg', width: 109, height: 60 },
		{ name: 'Ledgerly', src: '/logos/ledgerly.svg', width: 48, height: 48, scale: 0.75 }
	];
</script>

<CustomerLogoStrip label="Trusted by teams at" {logos} tone="mono" />

How logos are sized#

A fixed height makes a square mark look twice the size of a long wordmark. Each logo's height is the base size (32 px on phones, 36 px from sm, 40 px from lg) times ratio^-0.4, where ratio is width / height. A square mark sits at the full base height; a 6:1 wordmark sits at about half. The result is clamped between 40% and 100% of the base.

That only works if width and height describe the artwork itself:

  • Trim SVGs to the mark's edges. Built-in padding makes a logo look small.
  • Filled marks (an app icon, a solid badge) carry more weight than outlines at the same size; give them scale: 0.75 to 0.9.
  • Thin or light wordmarks, and marks stacked over small lettering, can take scale: 1.1 to 1.25.

Layout#

  • align="center" (default) centres the label and clusters up to six logos in one row. Longer lists balance their rows, six at most, with the last row centred.
  • align="between" starts the label on the start edge and spreads the logos edge to edge from lg. Below lg both layouts use two or three logos a row; between keeps them on the start edge.
  • motion="marquee" scrolls the list at about 40 px a second. It pauses while the pointer is over it or anything inside has focus, and the button at the end of the row pauses it for good. Visitors who ask for reduced motion get the static wrap.

Dark band retone#

Svelte
<div
	class="bg-zinc-950"
	style="
		--customer-logo-strip-muted: #a1a1aa;
		--customer-logo-strip-ink: #fafafa;
		--customer-logo-strip-hairline: rgb(255 255 255 / 0.12);
		--customer-logo-strip-accent: #fafafa;
		--customer-logo-strip-filter: brightness(0) invert(1) opacity(0.7);
	"
>
	<CustomerLogoStrip label="Trusted by teams at" {logos} tone="mono" />
</div>

The mono filter turns every mark into a white silhouette, so a logo with a knocked-out detail (a white letter in a coloured square) loses that detail. Check each logo on the dark band, or supply light versions and use tone="original".

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
logosCustomerLogo[]YesNoneLogos in display order. Each has name (alt text and link name), src, width and height (the image's proportions), optional href and optional scale (an optical nudge, default 1).
labelstringNoNoneShort line above the row, such as "Trusted by teams at". It also names the list. Omitted, no label renders.
listLabelstringNo'Customers'Accessible name for the list when there is no visible label.
motion'static' | 'marquee'No'static'static wraps into centred rows; marquee scrolls slowly and pauses on hover, focus and a visible button. Reduced motion always gets the static wrap.
tone'original' | 'mono'No'original'original shows each logo as supplied; mono applies --customer-logo-strip-filter, and linked logos regain their colours on hover and focus.
align'center' | 'between'No'center'center clusters the row under a centred label; between starts the label on the start edge and spreads the logos edge to edge from the lg breakpoint.
pauseLabelstringNo'Pause scrolling logos'Accessible name of the marquee button while it scrolls.
playLabelstringNo'Play scrolling logos'Accessible name of the marquee button while it is paused.

Customization#

On this page

Content comes through props. Retone the label, button, focus ring and mono filter through six --customer-logo-strip-* CSS variables; edit the Tailwind classes for logo size and spacing.

  • Logos: pass SVGs trimmed to the mark's edges with their real width and height. If one still looks too heavy or too light next to the others, set its scale (0.75 for a solid square badge, up to 1.25 for a mark stacked over small lettering).
  • Size: the base logo height is --_size on the section, 2rem on phones, 2.25rem from sm and 2.5rem from lg. A square mark renders at that height; wider marks render shorter by ratio^-0.4.
  • Mono tone: --customer-logo-strip-filter is the whole filter, default grayscale(1) contrast(1.15) opacity(0.7). Lower the opacity for a quieter row.
  • Label: --customer-logo-strip-muted colours it; keep 4.5:1 against the page.
  • Controls: --customer-logo-strip-ink sets the pause icon, --customer-logo-strip-hairline its ring and --customer-logo-strip-accent the focus ring on the button and linked logos.
  • Dark band retone: muted #a1a1aa, ink #fafafa, hairline rgb(255 255 255 / 0.12), accent #fafafa, filter brightness(0) invert(1) opacity(0.7). usage.md has the snippet.
  • Motion: motion="marquee" for long lists. Speed is about 40 px a second (3.75 s per logo in duration); change that number to go faster or slower.
  • Links: give href only where there is a real destination, such as a case study. Unlinked logos have no hover state.

Public CSS variables

VariableToken
--customer-logo-strip-accentaccent
--customer-logo-strip-on-accentonAccent
--customer-logo-strip-inkink
--customer-logo-strip-mutedmuted
--customer-logo-strip-hairlinehairline
--customer-logo-strip-filterfilter

Accessibility#

On this page
  • Logos are a list named by the visible label (aria-labelledby) or, without one, by listLabel.
  • Each image's alt text is the company name alone; a linked logo's link takes its name from that alt text.
  • The marquee's extra copies are aria-hidden with empty alt text, and their links carry tabindex="-1", so the list is announced once and tabbing visits each logo once. The copies stay clickable with a pointer, because a visitor can see them.
  • The marquee pauses while the pointer is over the logos or a logo link has focus, and a visible button pauses and resumes it (WCAG 2.2.2). The button's own focus does not hold it paused. Its accessible name switches between pauseLabel and playLabel.
  • Tabbing through a linked marquee stops the row and drops its edge fades, and each focused logo scrolls fully into view; the loop resumes from its start when focus leaves.
  • The marquee only starts once the component has hydrated, so the pause button always works while the row moves. Under prefers-reduced-motion it never moves and wraps into rows like the static layout.
  • Linked logos and the button have a two-pixel accent focus ring offset by two pixels on :focus-visible. Logo links are at least 44 px tall; the button is 32 px, growing to 44 px on coarse pointers.
  • Muted label text (#52525b) measures 7.7:1 on white.
  • Two instances on one page get distinct ids from $props.id().

Known limitations

  • The component cannot tell whether a logo's colours meet contrast on your page; mono logos at 70% opacity are decorative images named by alt text, not text.
  • Consumers are responsible for having permission to display every logo.

Release details#

On this page
Integration
  • Local interaction
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 30 September 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.