Skip to content
Brok UI

Loading…

No results

Video Embed

Open source

Click-to-load video facade: nothing from the video host loads until the visitor presses Play, then a sandboxed iframe replaces the facade and takes focus. Without JavaScript the facade is a plain link to the video.

Version
v1.1.0
Stability
stable
License
MIT
Related
Hero Video Dialog
Video Mask
Aspect Ratio
Consent Manager

Preview

previews.components.video-embed.default.blade.php Blade
{{-- The preview embeds a same-origin page, so nothing third-party loads even after Play. --}}
<div class="w-full max-w-xl">
    <x-ui.video-embed
        src="{{ url('/og/component/video-embed') }}"
        title="{{ __('Product tour: set up your first workspace') }}"
        provider="Brok"
    />
</div>

Installation

terminal
php artisan ui:add video-embed

Note

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

resources/js/ui/index.js JS
import './video-embed.js';

Registry contract

php artisan ui:add video-embed 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/video-embed.blade.php
  • js resources/js/ui/video-embed.js
Registry dependencies
aspect-ratio
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.

video-embed.md
# Brok UI: Video Embed (`video-embed`)

Click-to-load video facade: nothing from the video host loads until the visitor presses Play, then a sandboxed iframe replaces the facade and takes focus. Without JavaScript the facade is a plain link to the video.

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

## Install

```bash
php artisan ui:add video-embed
```

## Usage

```blade
{{-- The preview embeds a same-origin page, so nothing third-party loads even after Play. --}}
<div class="w-full max-w-xl">
    <x-ui.video-embed
        src="{{ url('/og/component/video-embed') }}"
        title="{{ __('Product tour: set up your first workspace') }}"
        provider="Brok"
    />
</div>
```

## Props

- `src` (string, default `null`) — Required embed URL, loaded only after Play. Must be an absolute http(s) URL or a root-relative path; javascript:, data: and other schemes throw.
- `title` (string, default `null`) — The iframe title, shown on the facade and part of the Play control's name. Required unless caption is given; a blank title then takes the caption. Give each video its real name, not a position such as Video 1.
- `caption` (string|null, default `null`) — Optional visible caption under the player. The root becomes a <figure> with the caption as its <figcaption>, and a blank title takes the caption.
- `startLabel` (string, default `Play video`) — Visible label on the Play control.
- `provider` (string|null, default `null`) — Display name of the host (for example YouTube). Renders the notice Loads content from <provider>.
- `poster` (string|null, default `null`) — Optional poster image URL. Use your own copy; a provider thumbnail URL would contact the provider before Play.
- `aspect` (string, default `16/9`) — Width/height ratio such as 16/9, 4 / 3 or 1. Anything else falls back to 16 / 9.
- `sandbox` (string, default `allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox`) — iframe sandbox tokens. Enough for YouTube and Vimeo players (scripts, their own storage, fullscreen presentation, opening the video on the provider site). An empty string drops the attribute.
- `referrerpolicy` (string, default `strict-origin-when-cross-origin`) — iframe referrer policy: the provider sees your origin, not the full page URL.
- `allow` (string, default `autoplay; encrypted-media; picture-in-picture; fullscreen`) — Permissions policy for the iframe: autoplay (the Play click already is the user's choice), encrypted-media (DRM players), picture-in-picture and fullscreen. No sensors, camera, microphone or clipboard. An empty string drops the attribute.
- `allowfullscreen` (bool, default `true`) — Adds the legacy allowfullscreen attribute, but only when allow does not already grant fullscreen: allow takes precedence and browsers warn when both are present. With the default allow it is not rendered.

## Use when

- Use when image or video selection, preview, or media-based comparison is part of the task.
- Embedding a third-party video (YouTube, Vimeo, a hosted player) inline in content without loading the provider before the visitor asks for it.
- Keeping a page fast and free of third-party requests until Play, with a visible notice of which provider the video loads from.

## Avoid when

- Do not require media-heavy interaction when a simpler text or list solution would be faster.
- The video should open in a focused lightbox; use hero-video-dialog.
- A muted decorative background video; use video-mask.
- Your own video file; use a native video element or audio-player style controls instead of an iframe.

## Anti-patterns

- Requiring media-heavy interaction when text is faster

## Rules

- Use the `<brok:video-embed>` tag (or `<x-ui.video-embed>`) 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/video-embed
- Registry JSON (files, props, contract): https://brokui.dev/r/open/video-embed.json

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

Examples

long-content.blade.php Blade
<div class="w-full max-w-xs">
    <x-ui.video-embed
        src="{{ url('/og/component/video-embed') }}"
        title="{{ __('A deliberately long video title that has to wrap inside a narrow facade without pushing the Play control out of the frame: Donaudampfschifffahrtsgesellschaftskapitän') }}"
        provider="{{ __('A video host with a long display name') }}"
        aspect="4/3"
    />
</div>
with-poster.blade.php Blade
{{-- A poster from your own origin is fine: it is not a provider request. --}}
<div class="w-full max-w-xl">
    <x-ui.video-embed
        src="{{ url('/og/component/video-embed') }}"
        poster="{{ asset('og-default.png') }}"
        title="{{ __('Product tour: set up your first workspace') }}"
        start-label="{{ __('Watch the tour') }}"
        provider="Brok"
    />
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
src string null Required embed URL, loaded only after Play. Must be an absolute http(s) URL or a root-relative path; javascript:, data: and other schemes throw.
title string null The iframe title, shown on the facade and part of the Play control's name. Required unless caption is given; a blank title then takes the caption. Give each video its real name, not a position such as Video 1.
caption string | null null Optional visible caption under the player. The root becomes a <figure> with the caption as its <figcaption>, and a blank title takes the caption.
startLabel string Play video Visible label on the Play control.
provider string | null null Display name of the host (for example YouTube). Renders the notice Loads content from <provider>.
poster string | null null Optional poster image URL. Use your own copy; a provider thumbnail URL would contact the provider before Play.
aspect string 16/9 Width/height ratio such as 16/9, 4 / 3 or 1. Anything else falls back to 16 / 9.
sandbox string allow-scripts allow-same-origin allow-presentati... iframe sandbox tokens. Enough for YouTube and Vimeo players (scripts, their own storage, fullscreen presentation, opening the video on the provider site). An empty string drops the attribute.
referrerpolicy string strict-origin-when-cross-origin iframe referrer policy: the provider sees your origin, not the full page URL.
allow string autoplay; encrypted-media; picture-in-picture; f... Permissions policy for the iframe: autoplay (the Play click already is the user's choice), encrypted-media (DRM players), picture-in-picture and fullscreen. No sensors, camera, microphone or clipboard. An empty string drops the attribute.
allowfullscreen bool true Adds the legacy allowfullscreen attribute, but only when allow does not already grant fullscreen: allow takes precedence and browsers warn when both are present. With the default allow it is not rendered.

Slots

Default Blade slot only.

Data slots

Stable hooks for CSS overrides and browser tests.

video-embed video-embed-caption video-embed-iframe video-embed-label video-embed-notice video-embed-play video-embed-poster video-embed-title

Behavior

  • Before Play the component renders no iframe, no provider script and no provider image: the iframe lives in an x-if template.
  • The facade is a link to src. With JavaScript it takes role button; a click, Enter or Space inserts the iframe in place, moves focus to it and dispatches a bubbling video-embed-play event with detail { src }. A middle click still opens the link.
  • The component never adds autoplay parameters. Add the provider's autoplay parameter to src when one click should also start playback.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, behaviorTest, authoredStateFixtures, responsive, rtl, darkMode, localized.

Guidance

Media selection and presentation

Select, preview, or compare visual media.

Use when

  • Use when image or video selection, preview, or media-based comparison is part of the task.
  • Embedding a third-party video (YouTube, Vimeo, a hosted player) inline in content without loading the provider before the visitor asks for it.
  • Keeping a page fast and free of third-party requests until Play, with a visible notice of which provider the video loads from.

Avoid when

  • Do not require media-heavy interaction when a simpler text or list solution would be faster.
  • The video should open in a focused lightbox; use hero-video-dialog.
  • A muted decorative background video; use video-mask.
  • Your own video file; use a native video element or audio-player style controls instead of an iframe.

Use instead

  • Text or list representation

Anti-patterns

  • Requiring media-heavy interaction when text is faster
Anatomy
video-embed aspect-ratio video-embed-play video-embed-poster video-embed-label video-embed-title video-embed-iframe video-embed-caption video-embed-notice
Theming hooks
video-embed aspect-ratio

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
Enter Space Tab
Focus
Focus moves to the iframe after Play.
  • The Play control's name is its visible label plus the video title; the iframe carries the same title.
  • Focus moves into the iframe after Play, so keyboard users land on the player.
  • Without JavaScript the facade is an ordinary link to the video.
  • 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="video-embed-{{ $record->id }}">
    {{-- The preview embeds a same-origin page, so nothing third-party loads even after Play. --}}
    <div class="w-full max-w-xl">
        <x-ui.video-embed
            src="{{ url('/og/component/video-embed') }}"
            title="{{ __('Product tour: set up your first workspace') }}"
            provider="Brok"
        />
    </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/video-embed.blade.php Blade
@props([
    // Embed URL, loaded only after Play. Absolute http(s) or a root-relative path.
    'src' => null,
    // The iframe title, and part of the Play control's name. Required unless
    // `caption` is given: a blank title then takes the caption.
    'title' => null,
    // Optional visible caption under the player. With a caption the root is a
    // <figure> and the caption its <figcaption>.
    'caption' => null,
    // Visible label on the Play control. Defaults to "Play video".
    'startLabel' => 'Play video',
    // Display name of the host, shown as "Loads content from <provider>".
    'provider' => null,
    // Optional poster image URL. Use your own copy: a third-party thumbnail
    // URL would load from the provider before the visitor chose to play.
    'poster' => null,
    // Width / height ratio, for example "16/9", "4 / 3" or "1".
    'aspect' => '16/9',
    // Iframe defaults. Each one is overridable; pass an empty string to drop
    // `sandbox` or `allow` when a provider needs more than the default.
    'sandbox' => 'allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox',
    'referrerpolicy' => 'strict-origin-when-cross-origin',
    'allow' => 'autoplay; encrypted-media; picture-in-picture; fullscreen',
    // Legacy attribute. It renders only when `allow` does not already grant
    // fullscreen: `allow` takes precedence and browsers warn about both.
    'allowfullscreen' => true,
])

@php
    $embedUrl = trim((string) $src);
    $scheme = strtolower((string) parse_url($embedUrl, PHP_URL_SCHEME));
    $isAbsoluteHttp = in_array($scheme, ['http', 'https'], true) && filled(parse_url($embedUrl, PHP_URL_HOST));
    $isRootRelative = str_starts_with($embedUrl, '/') && ! str_starts_with($embedUrl, '//') && ! str_contains($embedUrl, '\\');
    if (! $isAbsoluteHttp && ! $isRootRelative) {
        throw new \InvalidArgumentException('video-embed only accepts an http(s) or root-relative src.');
    }
    if (blank($title) && filled($caption)) {
        $title = $caption;
    }
    if (blank($title)) {
        throw new \InvalidArgumentException('video-embed needs a non-empty title.');
    }

    $hasCaption = filled($caption);
    $rootTag = $hasCaption ? 'figure' : 'div';
    $allowsFullscreen = preg_match('/(?:^|;)\s*fullscreen(?:\s|;|$)/i', (string) $allow) === 1;
    $label = __(filled($startLabel) ? $startLabel : 'Play video');
    $ratio = preg_match('/^\s*\d+(?:\.\d+)?\s*(?:\/\s*\d+(?:\.\d+)?\s*)?$/', (string) $aspect) === 1
        ? preg_replace('/\s*\/\s*/', ' / ', trim((string) $aspect))
        : '16 / 9';
@endphp

<{{ $rootTag }}
    x-data="uiVideoEmbed({ src: @js($embedUrl) })"
    data-slot="video-embed"
    :data-state="playing ? 'playing' : 'idle'"
    data-state="idle"
    {{ $attributes->merge(['class' => 'flex min-w-0 max-w-full flex-col gap-2']) }}
>
    <x-ui.aspect-ratio :ratio="$ratio" class="rounded-lg border border-border bg-muted">
        {{-- The facade is a real link, so it still reaches the video without
             JavaScript. With JavaScript it becomes the Play button: a primary
             click or Space loads the iframe in place; a middle click still
             opens the link. --}}
        <a
            href="{{ $embedUrl }}"
            rel="noopener noreferrer"
            data-slot="video-embed-play"
            x-show="!playing"
            x-bind:role="'button'"
            @click.prevent="play()"
            @keydown.space.prevent="play()"
            class="group absolute inset-0 flex flex-col items-center justify-center gap-3 overflow-hidden p-4 text-center text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-inset focus-visible:ring-ring"
        >
            @if (filled($poster))
                <img src="{{ $poster }}" alt="" loading="lazy" decoding="async" data-slot="video-embed-poster" class="absolute inset-0 size-full object-cover" />
                <span class="absolute inset-0 bg-gradient-to-t from-background/90 via-background/40 to-transparent" aria-hidden="true"></span>
            @endif

            <span class="relative inline-flex size-12 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-md" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="currentColor" class="size-6">
                    <path d="M8 5.14v13.72a1 1 0 0 0 1.54.84l10.29-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" />
                </svg>
            </span>
            <span class="relative flex min-w-0 max-w-full flex-col gap-1">
                <span data-slot="video-embed-label" class="text-sm font-medium">{{ $label }}</span>
                <span data-slot="video-embed-title" class="line-clamp-2 break-words text-sm text-muted-foreground">{{ $title }}</span>
            </span>
        </a>

        <template x-if="playing">
            <iframe
                x-ref="frame"
                src="{{ $embedUrl }}"
                title="{{ $title }}"
                data-slot="video-embed-iframe"
                @if (filled($sandbox)) sandbox="{{ $sandbox }}" @endif
                referrerpolicy="{{ $referrerpolicy }}"
                @if (filled($allow)) allow="{{ $allow }}" @endif
                loading="lazy"
                @if ($allowfullscreen && ! $allowsFullscreen) allowfullscreen @endif
                class="absolute inset-0 size-full border-0"
            ></iframe>
        </template>
    </x-ui.aspect-ratio>

    @if ($hasCaption)
        <figcaption data-slot="video-embed-caption" class="break-words text-sm text-muted-foreground">{{ $caption }}</figcaption>
    @endif

    @if (filled($provider))
        <p data-slot="video-embed-notice" class="break-words text-xs text-muted-foreground">
            {{ __('Loads content from :provider', ['provider' => $provider]) }}
        </p>
    @endif
</{{ $rootTag }}>
resources/js/ui/video-embed.js JS
/**
 * Video Embed behaviour: a click-to-load facade.
 *
 * Nothing from the video host loads before the visitor presses Play: the
 * iframe lives in an `x-if` template, so it is not in the document (and its
 * `src` is not requested) until `play()` runs. The facade is a real link to
 * the video, so it still works without JavaScript.
 *
 * `play()` inserts the iframe, moves focus to it and dispatches a bubbling
 * `video-embed-play` event with `{ src }`. The component adds no global
 * listeners, so `destroy()` has nothing to remove.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiVideoEmbed', (config = {}) => ({
        src: typeof config.src === 'string' ? config.src : '',
        playing: false,

        play() {
            if (this.playing) {
                return;
            }
            this.playing = true;
            this.$nextTick(() => {
                this.$refs.frame?.focus();
                this.$dispatch('video-embed-play', { src: this.src });
            });
        },
    }));
});

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.0
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