Skip to content
Brok UI

Loading…

No results

Carousels

Open source

Scrolling card carousels — a flat slide-and-snap track, a 3D perspective coverflow, a rotating 3D ring and a centre-emphasis phone-screen coverflow.

Version
v1.1.1
Stability
stable
License
MIT
Related
Carousel
3D Card Carousel
Cylinder Carousel
Scrollable Card Stack
Progressive Carousel
6 of 6

Phone Showcase — A center-emphasis coverflow carousel that showcases app screens inside iPhone frames, with the active phone scaled up and neighbours dimmed.

Preview

previews.components.phone-showcase.default.blade.php Blade
<div class="px-4 py-8">
    <x-ui.phone-showcase />
</div>

Installation

terminal
php artisan ui:add phone-showcase

Note

This component ships an Alpine behavior module at resources/js/ui/phone-showcase.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './phone-showcase.js';

Registry contract

php artisan ui:add phone-showcase writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/phone-showcase.blade.php
  • js resources/js/ui/phone-showcase.js
Registry dependencies
iphone-mockup carousel
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

phone-showcase.md
# Brok UI: Phone Showcase (`phone-showcase`)

A center-emphasis coverflow carousel that showcases app screens inside iPhone frames, with the active phone scaled up and neighbours dimmed.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add phone-showcase
```

## Usage

```blade
<div class="px-4 py-8">
    <x-ui.phone-showcase />
</div>
```

## Props

- `screens` (mixed|null, default `null`) — App screens to show; each is {image, alt}, falling back to a built-in faux app UI when image is omitted.
- `autoplay` (bool, default `false`) — Advances automatically on a timer when true, gated behind reduced motion.
- `interval` (int, default `5000`) — Milliseconds between automatic advances when autoplay is true.
- `loop` (bool, default `true`) — Wraps from the last screen back to the first and vice versa when true.

## Use when

- Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
- Showcasing app screens or mobile UI as a marketing/product feature, framed inside a realistic iPhone mockup.
- Wanting a center-emphasis coverflow specifically tuned for portrait phone screens rather than generic cards.

## Avoid when

- Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
- The content is not phone/app screenshots; use carousel, card-carousel-3d or collection-surfer for general content.
- A full 360-degree ring effect is wanted; use carousel-cylinder instead.

## Anti-patterns

- Heavy effects in forms or task-dense workflows
- Motion that interferes with reading or focus

## Rules

- Use the `<brok:phone-showcase>` tag (or `<x-ui.phone-showcase>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/phone-showcase
- Registry JSON (files, props, contract): https://brokui.dev/r/open/phone-showcase.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
screens mixed | null null App screens to show; each is {image, alt}, falling back to a built-in faux app UI when image is omitted.
autoplay bool false Advances automatically on a timer when true, gated behind reduced motion.
interval int 5000 Milliseconds between automatic advances when autoplay is true.
loop bool true Wraps from the last screen back to the first and vice versa when true.

Slots

  • default — Unused; screens are rendered from the screens prop.

Data slots

Stable hooks for CSS overrides and browser tests.

phone-showcase

Behavior

  • The active phone is centred and shown at full scale while neighbouring phones are scaled down and dimmed; prev/next buttons, dot indicators and the arrow keys drive the index.
  • Optional autoplay advances on a timer and is disabled under prefers-reduced-motion.
  • Each screen falls back to a token-built faux app UI when no image is supplied, so the component renders meaningfully with zero props.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.

Guidance

Expressive and showcase effect

Support brand expression or a showcase moment.

Use when

  • Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
  • Showcasing app screens or mobile UI as a marketing/product feature, framed inside a realistic iPhone mockup.
  • Wanting a center-emphasis coverflow specifically tuned for portrait phone screens rather than generic cards.

Avoid when

  • Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
  • The content is not phone/app screenshots; use carousel, card-carousel-3d or collection-surfer for general content.
  • A full 360-degree ring effect is wanted; use carousel-cylinder instead.

Use instead

  • Static content-first presentation

Anti-patterns

  • Heavy effects in forms or task-dense workflows
  • Motion that interferes with reading or focus
Anatomy
root
Theming hooks
phone-showcase

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
ArrowLeft ArrowRight
Focus
managed
  • The root is role=region with aria-roledescription=carousel and a descriptive aria-label; each screen is role=group aria-roledescription=slide with an aria-label stating its position.
  • Arrow-key navigation follows reading order and swaps under RTL.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="phone-showcase-{{ $record->id }}">
    <div class="px-4 py-8">
        <x-ui.phone-showcase />
    </div>
</div>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/phone-showcase.blade.php Blade
@props([
    'screens' => null,
    'autoplay' => false,
    'interval' => 5000,
    'loop' => true,
])

@php
    // Default to three faux app screens so the component renders with zero args.
    $screens = $screens ?: [
        ['image' => null, 'alt' => 'App home screen'],
        ['image' => null, 'alt' => 'App activity screen'],
        ['image' => null, 'alt' => 'App profile screen'],
    ];
@endphp

{{--
    Phone showcase — a center-emphasis ("coverflow") carousel of app screens
    inside iPhone frames. The active phone is centered and full-scale; its
    neighbours are scaled down and dimmed for depth. Prev/next buttons, dot
    indicators and arrow keys drive it; RTL-aware. Optional autoplay is disabled
    under prefers-reduced-motion (handled in phone-showcase.js).
--}}
<div
    x-data="uiPhoneShowcase({
        loop: @js((bool) $loop),
        autoplay: @js((bool) $autoplay),
        interval: @js((int) $interval),
    })"
    data-slot="phone-showcase"
    role="region"
    aria-roledescription="carousel"
    aria-label="{{ __('App screen showcase') }}"
    tabindex="0"
    @keydown="onKey($event)"
    {{ $attributes->merge(['class' => 'relative flex flex-col items-center gap-6 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring']) }}
>
    {{-- Coverflow viewport: the track is centered, slides translate by active index. --}}
    <div class="relative w-full overflow-hidden">
        <div
            x-ref="track"
            class="flex items-center justify-center"
            :style="`transform: translateX(calc(${(getComputedStyle($el).direction === 'rtl' ? 1 : -1) * index} * 16rem))`"
            style="transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1)"
        >
            @foreach ($screens as $i => $screen)
                <div
                    class="shrink-0 px-2 transition-all duration-500 ease-out motion-reduce:transition-none"
                    :class="isActive(@js($i)) ? 'scale-100 opacity-100 z-10' : 'scale-90 opacity-60'"
                    :aria-hidden="isActive(@js($i)) ? 'false' : 'true'"
                    role="group"
                    aria-roledescription="{{ __('slide') }}"
                    aria-label="{{ __('Screen :n of :total', ['n' => $i + 1, 'total' => count($screens)]) }}"
                >
                    <x-ui.iphone-mockup :label="__('App screen')">
                        @if (!empty($screen['image']))
                            <img
                                src="{{ $screen['image'] }}"
                                alt="{{ __($screen['alt'] ?? 'App screen') }}"
                                class="size-full object-cover"
                                loading="lazy"
                                decoding="async"
                            />
                        @else
                            {{-- Faux app screen built from tokens so it reads as a real app. --}}
                            <div class="flex size-full flex-col gap-3 bg-background p-4" aria-hidden="true">
                                {{-- Header bar --}}
                                <div class="flex items-center justify-between rounded-md bg-card p-3 shadow-sm">
                                    <span class="h-2 w-12 rounded-full bg-muted"></span>
                                    <span class="size-6 rounded-full bg-muted"></span>
                                </div>
                                {{-- Hero block --}}
                                <div class="flex h-24 flex-col justify-end gap-2 rounded-md bg-primary/10 p-3">
                                    <span class="h-2 w-2/3 rounded-full bg-primary/30"></span>
                                    <span class="h-2 w-1/2 rounded-full bg-primary/20"></span>
                                </div>
                                {{-- Content rows --}}
                                <div class="flex flex-1 flex-col gap-3">
                                    <div class="flex items-center gap-3 rounded-md bg-card p-2">
                                        <span class="size-8 shrink-0 rounded-md bg-muted"></span>
                                        <span class="flex flex-1 flex-col gap-2">
                                            <span class="h-2 w-3/4 rounded-full bg-muted"></span>
                                            <span class="h-2 w-1/2 rounded-full bg-muted"></span>
                                        </span>
                                    </div>
                                    <div class="flex items-center gap-3 rounded-md bg-card p-2">
                                        <span class="size-8 shrink-0 rounded-md bg-muted"></span>
                                        <span class="flex flex-1 flex-col gap-2">
                                            <span class="h-2 w-2/3 rounded-full bg-muted"></span>
                                            <span class="h-2 w-2/5 rounded-full bg-muted"></span>
                                        </span>
                                    </div>
                                </div>
                                {{-- CTA bar --}}
                                <div class="flex h-10 items-center justify-center rounded-md bg-primary">
                                    <span class="h-2 w-1/3 rounded-full bg-primary-foreground/80"></span>
                                </div>
                            </div>
                        @endif
                    </x-ui.iphone-mockup>
                </div>
            @endforeach
        </div>
    </div>

    {{-- Controls: prev/next + dot indicators. phone-showcase's Alpine scope
         already uses carousel.previous/next/dots' own defaults (prev()/next()/
         canPrev()/canNext()/index/goTo), so only the visual classes need
         overriding to keep this control bar's inline (not overlay) look. --}}
    <div class="flex items-center gap-4">
        <x-ui.carousel.previous
            aria-label="{{ __('Previous screen') }}"
            class="!static !size-10 !bg-card hover:!bg-accent hover:!text-accent-foreground"
        />

        <x-ui.carousel.dots :count="count($screens)" class="gap-1" />

        <x-ui.carousel.next
            aria-label="{{ __('Next screen') }}"
            class="!static !size-10 !bg-card hover:!bg-accent hover:!text-accent-foreground"
        />
    </div>
</div>
resources/js/ui/phone-showcase.js JS
/**
 * Phone showcase behavior — a center-emphasis ("coverflow") carousel of phones.
 *
 * Tracks the active index on the root component; the Blade scales/dims each
 * slide by comparing its position to `index`. Direction-aware: in RTL the track
 * shifts the opposite way and ArrowLeft/ArrowRight are swapped so navigation
 * always follows reading order. `loop` wraps past either end. Optional autoplay
 * runs only when the user has not requested reduced motion, and its timer is
 * cleared on teardown so unmounting mid-cycle never leaks.
 *
 * Self-registers on `alpine:init` so import order does not matter — import this
 * file once from your bundle (e.g. resources/js/ui/index.js).
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiPhoneShowcase', (config = {}) => ({
        index: 0,
        loop: config.loop ?? true,
        autoplay: config.autoplay ?? false,
        interval: config.interval ?? 5000,
        timer: null,

        init() {
            const reduced =
                window.matchMedia &&
                window.matchMedia('(prefers-reduced-motion: reduce)').matches;

            if (this.autoplay && !reduced && this.count() > 1) {
                this.timer = window.setInterval(() => {
                    this.next();
                }, this.interval);
            }
        },

        destroy() {
            if (this.timer !== null) {
                window.clearInterval(this.timer);
                this.timer = null;
            }
        },

        count() {
            return this.$refs.track ? this.$refs.track.children.length : 0;
        },

        isActive(i) {
            return this.index === i;
        },

        goTo(i) {
            const total = this.count();
            if (total === 0) {
                return;
            }
            this.index = Math.max(0, Math.min(i, total - 1));
        },

        next() {
            if (this.index < this.count() - 1) {
                this.index++;
            } else if (this.loop) {
                this.index = 0;
            }
        },

        prev() {
            if (this.index > 0) {
                this.index--;
            } else if (this.loop) {
                this.index = this.count() - 1;
            }
        },

        canPrev() {
            return this.loop || this.index > 0;
        },

        canNext() {
            return this.loop || this.index < this.count() - 1;
        },

        onKey(e) {
            const rtl = getComputedStyle(this.$el).direction === 'rtl';
            if (e.key === 'ArrowRight') {
                e.preventDefault();
                rtl ? this.prev() : this.next();
            } else if (e.key === 'ArrowLeft') {
                e.preventDefault();
                rtl ? this.next() : this.prev();
            }
        },
    }));
});

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
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0