Logo Cloud
Trusted-by logo clouds — a bordered grid (with an optional cursor-spotlight reveal), a marquee, an inline-heading row and an animated group cycler.
Preview
@props([
'heading' => 'Trusted by teams using',
'logos' => null,
'groupSize' => 4,
'interval' => 2500,
])
@php
$logos ??= [
'Northwind',
'Acme Corp',
'Globex',
'Initech',
'Umbrella',
'Soylent',
'Hooli',
'Vehement',
'Stripe',
'Vercel',
];
// Translate up-front so Alpine renders already-localized labels.
$logoLabels = array_values(array_map(fn ($logo) => __($logo), $logos));
@endphp
<section {{ $attributes->merge(['class' => 'bg-background py-16 text-foreground sm:py-20']) }}>
<div
class="mx-auto max-w-6xl px-4"
x-data="uiLogoCloudCycle({
logos: @js($logoLabels),
groupSize: {{ (int) $groupSize }},
interval: {{ (int) $interval }},
})"
>
@if ($heading)
<p class="text-center text-sm font-medium uppercase tracking-wider text-muted-foreground">
{{ __($heading) }}
</p>
<x-ui.separator class="mx-auto mt-6 max-w-24" />
@endif
{{-- Static, screen-reader / SEO copy: every brand exists in the DOM regardless of which group is on stage. --}}
<ul class="sr-only">
@foreach ($logoLabels as $logo)
<li>{{ $logo }}</li>
@endforeach
</ul>
{{-- Animated stage: aria-hidden so the cycling chips don't spam assistive tech. --}}
<div
class="mt-10 flex min-h-24 items-center justify-center"
aria-hidden="true"
@mouseenter="pause()"
@mouseleave="resume()"
>
<ul class="flex flex-wrap items-center justify-center gap-x-6 gap-y-4 sm:gap-x-10">
<template x-for="(logo, i) in current" :key="index + '-' + i">
<li
class="inline-flex items-center justify-center rounded-md border border-border bg-card px-6 py-2 text-base font-semibold tracking-tight text-muted-foreground opacity-0 shadow-sm transition-[opacity,filter,color] duration-500 ease-out [filter:blur(4px)] motion-reduce:transition-none motion-reduce:opacity-100 motion-reduce:[filter:none] hover:text-foreground"
:class="shown ? 'opacity-100 [filter:blur(0)]' : 'opacity-0 [filter:blur(4px)]'"
:style="`transition-delay:${i * 80}ms`"
x-text="logo"
></li>
</template>
</ul>
</div>
</div>
</section>
/**
* Cycling logo cloud behavior.
*
* Splits a flat list of (already-localized) brand labels into fixed-size groups
* and rotates through them on a timer. Each transition fades + un-blurs the
* incoming group in (per-item staggered delay handled in the Blade `:style`),
* after briefly hiding the outgoing one. All labels also live in a static
* `sr-only` list in the markup, so this animated stage is purely visual and is
* marked `aria-hidden`.
*
* Under `prefers-reduced-motion: reduce` the cycle never starts (the Blade's
* `motion-reduce:` variants only remove the fade — a 2.5 s content swap is
* still motion). The rotation pauses while the pointer or keyboard focus is
* inside the stage, so the content can be read — WCAG 2.2.2 (Pause, Stop,
* Hide) for auto-updating content. Bind `@mouseenter="pause()"`,
* `@mouseleave="resume()"`, `@focusin="pause()"`, `@focusout="resume()"` on
* the stage.
*
* Self-registers on `alpine:init` so import order does not matter. The rotation
* timer and any pending swap timeout are cleared in `destroy()` to avoid leaks
* when the section unmounts mid-cycle.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiLogoCloudCycle', (config = {}) => ({
logos: Array.isArray(config.logos) ? config.logos : [],
groupSize: Math.max(1, parseInt(config.groupSize, 10) || 4),
interval: Math.max(600, parseInt(config.interval, 10) || 2500),
index: 0,
shown: true,
timer: null,
swap: null,
paused: false,
init() {
// Single-group lists have nothing to cycle through — show and stop.
if (this.groups.length <= 1) return;
// Re-read live, not once: an OS toggle mid-session must take effect.
this._mq = window.matchMedia('(prefers-reduced-motion: reduce)');
this._onMq = () => (this._mq.matches ? this.stop() : this.start());
this._mq.addEventListener('change', this._onMq);
this._onMq();
},
destroy() {
this.stop();
if (this.swap) clearTimeout(this.swap);
if (this._mq && this._onMq) this._mq.removeEventListener('change', this._onMq);
},
start() {
if (this.timer || this.paused || this.groups.length <= 1) return;
this.timer = setInterval(() => this.advance(), this.interval);
},
stop() {
if (this.timer) clearInterval(this.timer);
this.timer = null;
},
pause() {
this.paused = true;
this.stop();
},
resume() {
this.paused = false;
if (!this._mq?.matches) this.start();
},
// Chunk the flat list into groups of `groupSize`.
get groups() {
const out = [];
for (let i = 0; i < this.logos.length; i += this.groupSize) {
out.push(this.logos.slice(i, i + this.groupSize));
}
return out.length ? out : [[]];
},
get current() {
return this.groups[this.index] ?? [];
},
// Fade the current group out, swap to the next, then fade it back in.
advance() {
this.shown = false;
if (this.swap) clearTimeout(this.swap);
this.swap = setTimeout(() => {
const len = this.groups.length;
this.index = (this.index + 1) % len;
this.shown = true;
}, 350);
},
}));
});
Installation
php artisan ui:add blocks/logo-cloud-cycle
Registry contract
Install confidence
php artisan ui:add blocks/logo-cloud-cycle
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.1.1
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/logo-cloud-cycle.blade.php |
| js | resources/js/ui/logo-cloud-cycle.js |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
npm: alpinejs
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Logo Cloud Cycle (`logo-cloud-cycle`)
An animated trusted-by logo cloud that cycles through groups of wordmark chips with a staggered blur-and-fade transition between groups.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/logo-cloud-cycle
```
## Render it
```blade
<x-blocks.logo-cloud-cycle />
```
## Props
- `heading` (string, default `Trusted by teams using`) — Centered muted label above the logo cloud; falsy hides it and its separator.
- `logos` (mixed|null, default `null`) — Array of logo name strings; falls back to a 10-item sample list when null.
- `group-size` (int, default `4`) — Number of logo chips shown per cycling group.
- `interval` (int, default `2500`) — Milliseconds between automatic group rotations.
## Use when
- Use it for a 'trusted by' section that automatically cycles through groups of logo chips with a staggered blur-and-fade transition, fitting many brands into a small footprint.
- Reach for it when the interval-based rotation (configurable via groupSize/interval) and pause-on-hover behavior are wanted; every logo still exists in the DOM for SEO/screen readers.
## Avoid when
- Avoid it when all logos should be visible simultaneously without waiting for a rotation; use logo-cloud-grid or logo-cloud-cards instead.
## Rules
- Render the installed block with `<x-blocks.logo-cloud-cycle />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/logo-cloud-cycle
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/logo-cloud-cycle.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Use it for a 'trusted by' section that automatically cycles through groups of logo chips with a staggered blur-and-fade transition, fitting many brands into a small footprint.
- Reach for it when the interval-based rotation (configurable via groupSize/interval) and pause-on-hover behavior are wanted; every logo still exists in the DOM for SEO/screen readers.
Avoid when
- Avoid it when all logos should be visible simultaneously without waiting for a rotation; use logo-cloud-grid or logo-cloud-cards instead.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
Usage
Render the block as a component, passing data where useful:
<x-blocks.logo-cloud-cycle />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable files ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'heading' => 'Trusted by teams using',
'logos' => null,
'groupSize' => 4,
'interval' => 2500,
])
@php
$logos ??= [
'Northwind',
'Acme Corp',
'Globex',
'Initech',
'Umbrella',
'Soylent',
'Hooli',
'Vehement',
'Stripe',
'Vercel',
];
// Translate up-front so Alpine renders already-localized labels.
$logoLabels = array_values(array_map(fn ($logo) => __($logo), $logos));
@endphp
<section {{ $attributes->merge(['class' => 'bg-background py-16 text-foreground sm:py-20']) }}>
<div
class="mx-auto max-w-6xl px-4"
x-data="uiLogoCloudCycle({
logos: @js($logoLabels),
groupSize: {{ (int) $groupSize }},
interval: {{ (int) $interval }},
})"
>
@if ($heading)
<p class="text-center text-sm font-medium uppercase tracking-wider text-muted-foreground">
{{ __($heading) }}
</p>
<x-ui.separator class="mx-auto mt-6 max-w-24" />
@endif
{{-- Static, screen-reader / SEO copy: every brand exists in the DOM regardless of which group is on stage. --}}
<ul class="sr-only">
@foreach ($logoLabels as $logo)
<li>{{ $logo }}</li>
@endforeach
</ul>
{{-- Animated stage: aria-hidden so the cycling chips don't spam assistive tech. --}}
<div
class="mt-10 flex min-h-24 items-center justify-center"
aria-hidden="true"
@mouseenter="pause()"
@mouseleave="resume()"
>
<ul class="flex flex-wrap items-center justify-center gap-x-6 gap-y-4 sm:gap-x-10">
<template x-for="(logo, i) in current" :key="index + '-' + i">
<li
class="inline-flex items-center justify-center rounded-md border border-border bg-card px-6 py-2 text-base font-semibold tracking-tight text-muted-foreground opacity-0 shadow-sm transition-[opacity,filter,color] duration-500 ease-out [filter:blur(4px)] motion-reduce:transition-none motion-reduce:opacity-100 motion-reduce:[filter:none] hover:text-foreground"
:class="shown ? 'opacity-100 [filter:blur(0)]' : 'opacity-0 [filter:blur(4px)]'"
:style="`transition-delay:${i * 80}ms`"
x-text="logo"
></li>
</template>
</ul>
</div>
</div>
</section>
/**
* Cycling logo cloud behavior.
*
* Splits a flat list of (already-localized) brand labels into fixed-size groups
* and rotates through them on a timer. Each transition fades + un-blurs the
* incoming group in (per-item staggered delay handled in the Blade `:style`),
* after briefly hiding the outgoing one. All labels also live in a static
* `sr-only` list in the markup, so this animated stage is purely visual and is
* marked `aria-hidden`.
*
* Under `prefers-reduced-motion: reduce` the cycle never starts (the Blade's
* `motion-reduce:` variants only remove the fade — a 2.5 s content swap is
* still motion). The rotation pauses while the pointer or keyboard focus is
* inside the stage, so the content can be read — WCAG 2.2.2 (Pause, Stop,
* Hide) for auto-updating content. Bind `@mouseenter="pause()"`,
* `@mouseleave="resume()"`, `@focusin="pause()"`, `@focusout="resume()"` on
* the stage.
*
* Self-registers on `alpine:init` so import order does not matter. The rotation
* timer and any pending swap timeout are cleared in `destroy()` to avoid leaks
* when the section unmounts mid-cycle.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiLogoCloudCycle', (config = {}) => ({
logos: Array.isArray(config.logos) ? config.logos : [],
groupSize: Math.max(1, parseInt(config.groupSize, 10) || 4),
interval: Math.max(600, parseInt(config.interval, 10) || 2500),
index: 0,
shown: true,
timer: null,
swap: null,
paused: false,
init() {
// Single-group lists have nothing to cycle through — show and stop.
if (this.groups.length <= 1) return;
// Re-read live, not once: an OS toggle mid-session must take effect.
this._mq = window.matchMedia('(prefers-reduced-motion: reduce)');
this._onMq = () => (this._mq.matches ? this.stop() : this.start());
this._mq.addEventListener('change', this._onMq);
this._onMq();
},
destroy() {
this.stop();
if (this.swap) clearTimeout(this.swap);
if (this._mq && this._onMq) this._mq.removeEventListener('change', this._onMq);
},
start() {
if (this.timer || this.paused || this.groups.length <= 1) return;
this.timer = setInterval(() => this.advance(), this.interval);
},
stop() {
if (this.timer) clearInterval(this.timer);
this.timer = null;
},
pause() {
this.paused = true;
this.stop();
},
resume() {
this.paused = false;
if (!this._mq?.matches) this.start();
},
// Chunk the flat list into groups of `groupSize`.
get groups() {
const out = [];
for (let i = 0; i < this.logos.length; i += this.groupSize) {
out.push(this.logos.slice(i, i + this.groupSize));
}
return out.length ? out : [[]];
},
get current() {
return this.groups[this.index] ?? [];
},
// Fade the current group out, swap to the next, then fade it back in.
advance() {
this.shown = false;
if (this.swap) clearTimeout(this.swap);
this.swap = setTimeout(() => {
const len = this.groups.length;
this.index = (this.index + 1) % len;
this.shown = true;
}, 350);
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.1 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0