Skip to content
Brok UI

Loading…

No results

Twitter Card

Open source

A Twitter/X post card with author, verified tick, post text, optional media, a link to the post, an optional default slot for extra content (a badge, a tag), and live reply, repost, like and bookmark controls that toggle and dispatch events.

Version
v1.2.2
Stability
stable
License
MIT
Related
Card
Link Preview
Chat Bubble

Preview

AL Ada Lovelace
Ada Lovelace
@ada

Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨

2:14 PM, 16 Jun 2026

212K
previews.components.twitter-card.default.blade.php Blade
<div class="mx-auto w-full max-w-md">
    <x-ui.twitter-card
        name="Ada Lovelace"
        handle="ada"
        :verified="true"
        text="Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨"
        time="2:14 PM, 16 Jun 2026"
        href="#"
        replies="128"
        retweets="1.2K"
        likes="8.4K"
        views="212K"
    />
</div>

Installation

terminal
php artisan ui:add twitter-card

Registry contract

php artisan ui:add twitter-card 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/twitter-card.blade.php
Registry dependencies
avatar
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.

twitter-card.md
# Brok UI: Twitter Card (`twitter-card`)

A Twitter/X post card with author, verified tick, post text, optional media, a link to the post, an optional default slot for extra content (a badge, a tag), and live reply, repost, like and bookmark controls that toggle and dispatch events.

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

## Install

```bash
php artisan ui:add twitter-card
```

## Usage

```blade
<div class="mx-auto w-full max-w-md">
    <x-ui.twitter-card
        name="Ada Lovelace"
        handle="ada"
        :verified="true"
        text="Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨"
        time="2:14 PM, 16 Jun 2026"
        href="#"
        replies="128"
        retweets="1.2K"
        likes="8.4K"
        views="212K"
    />
</div>
```

## Props

- `name` (string, default `Ada Lovelace`)
- `handle` (string, default `ada`)
- `avatar` (mixed|null, default `null`)
- `verified` (bool, default `true`)
- `text` (string, default `Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨`)
- `image` (mixed|null, default `null`)
- `time` (string, default `2:14 PM, 16 Jun 2026`)
- `replies` (string, default `128`)
- `retweets` (string, default `1.2K`)
- `likes` (string, default `8.4K`)
- `views` (string|null, default `212K`) — A falsy value (null or empty string) hides the views stat entirely.
- `href` (mixed|null, default `null`)
- `interactive` (bool, default `true`)

## Use when

- Use when self-contained content objects benefit from a recognizable card-like presentation with local actions or metadata.
- Embedding a post from X in a testimonial, changelog or social-proof section without the third-party script.
- Showing a post with working reply, repost, like and bookmark toggles that the page can listen to.

## Avoid when

- Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
- Rendering the live post; the content is passed in and never fetched.
- Building a chat message; chat-bubble is the conversational shape.

## Anti-patterns

- Using cards for dense cross-item comparison

## Rules

- Use the `<brok:twitter-card>` tag (or `<x-ui.twitter-card>`) 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/twitter-card
- Registry JSON (files, props, contract): https://brokui.dev/r/open/twitter-card.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
name string Ada Lovelace
handle string ada
avatar mixed | null null
verified bool true
text string Shipping a new design system today. Tokens all t...
image mixed | null null
time string 2:14 PM, 16 Jun 2026
replies string 128
retweets string 1.2K
likes string 8.4K
views string | null 212K A falsy value (null or empty string) hides the views stat entirely.
href mixed | null null
interactive bool true

Slots

  • default —

Data slots

Stable hooks for CSS overrides and browser tests.

twitter-card twitter-card-actions

Behavior

  • Reply, repost, like and bookmark toggle locally and dispatch events; interactive turns the action row off.
  • href links the post to its source; verified adds the tick, and image renders optional media.
  • Counts render as given, so formatting stays under the page's control.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Card and content widget

Present a self-contained content object with local context.

Use when

  • Use when self-contained content objects benefit from a recognizable card-like presentation with local actions or metadata.
  • Embedding a post from X in a testimonial, changelog or social-proof section without the third-party script.
  • Showing a post with working reply, repost, like and bookmark toggles that the page can listen to.

Avoid when

  • Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
  • Rendering the live post; the content is passed in and never fetched.
  • Building a chat message; chat-bubble is the conversational shape.

Use instead

  • List for compact scanning
  • Table for exact comparison

Anti-patterns

  • Using cards for dense cross-item comparison
Anatomy
root twitter-card-actions
Theming hooks
twitter-card twitter-card-actions

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • Each action is a button with a translated label and aria-pressed for its toggled state.
  • The verified mark is role="img" with a name, and decorative icons are aria-hidden.
  • 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="twitter-card-{{ $record->id }}">
    <div class="mx-auto w-full max-w-md">
        <x-ui.twitter-card
            name="Ada Lovelace"
            handle="ada"
            :verified="true"
            text="Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨"
            time="2:14 PM, 16 Jun 2026"
            href="#"
            replies="128"
            retweets="1.2K"
            likes="8.4K"
            views="212K"
        />
    </div>
</div>

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:twitter-card
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

Source

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

resources/views/components/ui/twitter-card.blade.php Blade
@props([
    'name' => 'Ada Lovelace',
    'handle' => 'ada',
    'avatar' => null,
    'verified' => true,
    'text' => 'Shipping a new design system today. Tokens all the way down — light, dark, RTL, and not a single hard-coded hex in sight. 🎨',
    'image' => null,
    'time' => '2:14 PM, 16 Jun 2026',
    'replies' => '128',
    'retweets' => '1.2K',
    'likes' => '8.4K',
    'views' => '212K',
    // Link to the post on X; the glyph and the timestamp open it.
    'href' => null,
    // The reply, repost, like and bookmark controls are live: repost, like and
    // bookmark toggle (a plain count goes up by one), reply and every toggle
    // dispatch `twitter-card:<action>` from the root for the host.
    'interactive' => true,
])

@php
    $handle = ltrim((string) $handle, '@');
    // The avatar fallback is two initials, not the full name squeezed into a circle.
    $initials = collect(preg_split('/\s+/', trim((string) $name)))->filter()->map(fn ($word) => mb_strtoupper(mb_substr($word, 0, 1)))->take(2)->implode('');
@endphp

{{--
    Twitter / X post card — a tweet preview. Header carries the author avatar,
    display name with an optional verified tick, @handle and the X glyph; the
    body renders the (auto-escaped) post text and an optional media image;
    a footer shows the timestamp and reply / repost / like / view metrics.
--}}
<article
    data-slot="twitter-card"
    @if ($interactive) x-data="{ reposted: false, liked: false, bookmarked: false, bump(count, on) { const n = Number(String(count).replace(/,/g, '')); return Number.isFinite(n) && /^[0-9,]+$/.test(String(count)) ? (n + (on ? 1 : 0)).toLocaleString() : count; } }" @endif
    {{ $attributes->merge(['class' => 'max-w-md rounded-lg border border-border bg-card p-6 text-foreground shadow-sm']) }}
>
    <header class="flex items-start gap-3">
        <x-ui.avatar size="md" :src="$avatar" :alt="$name" :fallback="$initials" class="shrink-0" />
        <div class="min-w-0 flex-1 leading-tight">
            <div class="flex items-center gap-1">
                <span class="truncate text-sm font-semibold">{{ $name }}</span>
                @if ($verified)
                    <svg aria-label="{{ __('Verified account') }}" role="img" viewBox="0 0 24 24" fill="currentColor" class="size-4 shrink-0 text-info">
                        <path d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.33 2.19c-1.4-.46-2.91-.2-3.92.81s-1.26 2.52-.8 3.91c-1.31.67-2.2 1.91-2.2 3.34s.89 2.67 2.2 3.34c-.46 1.39-.21 2.9.8 3.91s2.52 1.26 3.91.81c.67 1.31 1.91 2.19 3.34 2.19s2.68-.88 3.34-2.19c1.39.45 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34Zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77Z" />
                    </svg>
                @endif
            </div>
            <span class="block truncate text-sm text-muted-foreground">{{ '@'.$handle }}</span>
        </div>
        @php $xGlyph = '<svg viewBox="0 0 24 24" fill="currentColor" class="size-4 shrink-0" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z" /></svg>'; @endphp
        @if ($href)
            <a href="{{ $href }}" rel="noopener" aria-label="{{ __('View on X') }}" class="inline-flex size-8 shrink-0 items-center justify-center rounded-full text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none">{!! $xGlyph !!}</a>
        @else
            <span class="inline-flex size-8 shrink-0 items-center justify-center text-foreground" role="img" aria-label="{{ __('Posted on X') }}">{!! $xGlyph !!}</span>
        @endif
    </header>

    <p class="mt-3 whitespace-pre-line text-sm leading-relaxed">{{ $text }}</p>

    @if ($image)
        <div class="mt-3 overflow-hidden rounded-lg border border-border">
            <img src="{{ $image }}" alt="" class="aspect-video w-full object-cover" loading="lazy" />
        </div>
    @endif

    @if ($time)
        <p class="mt-3 text-xs text-muted-foreground">
            @if ($href)<a href="{{ $href }}" rel="noopener" class="underline-offset-4 hover:underline">{{ $time }}</a>@else{{ $time }}@endif
        </p>
    @endif

    {{-- Optional extra content (a "Verified buyer" badge, a tag, …) between the
         timestamp and the action row. --}}
    {{ $slot ?? '' }}

    @php
        // Each action: key, accessible label, glyph path, hover tint, the
        // pressed tint and whether the glyph fills when pressed.
        $actionButton = '-ms-2 inline-flex h-8 items-center gap-1 rounded-full px-2 text-xs tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none';
    @endphp
    <footer class="mt-3 flex items-center justify-between border-t border-border pt-3 text-xs text-muted-foreground" data-slot="twitter-card-actions">
        @if ($interactive)
            <button type="button" @click="$dispatch('twitter-card:reply')" class="{{ $actionButton }} hover:bg-info-soft hover:text-info-text" aria-label="{{ __('Reply') }}">
                <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5Z" /></svg>
                <span>{{ $replies }}</span>
            </button>
            <button type="button" @click="reposted = !reposted; $dispatch('twitter-card:repost', { on: reposted })" x-bind:aria-pressed="reposted ? 'true' : 'false'" x-bind:class="reposted ? 'text-success-text' : ''" class="{{ $actionButton }} hover:bg-success-soft hover:text-success-text" aria-label="{{ __('Repost') }}">
                <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M17 1l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3" /></svg>
                <span x-text="bump({{ \Illuminate\Support\Js::from((string) $retweets) }}, reposted)">{{ $retweets }}</span>
            </button>
            <button type="button" @click="liked = !liked; $dispatch('twitter-card:like', { on: liked })" x-bind:aria-pressed="liked ? 'true' : 'false'" x-bind:class="liked ? 'text-destructive-text' : ''" class="{{ $actionButton }} group/like hover:bg-destructive-soft hover:text-destructive-text" aria-label="{{ __('Like') }}">
                <svg aria-hidden="true" viewBox="0 0 24 24" x-bind:fill="liked ? 'currentColor' : 'none'" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4 transition-transform motion-safe:group-active/like:scale-125 motion-reduce:transition-none"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1 7.8 7.8 7.8-7.8 1-1a5.5 5.5 0 0 0 0-7.8Z" /></svg>
                <span x-text="bump({{ \Illuminate\Support\Js::from((string) $likes) }}, liked)">{{ $likes }}</span>
            </button>
            @if ($views)
                <span class="inline-flex h-8 items-center gap-1 px-2 tabular-nums" aria-label="{{ __('Views') }}">
                    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M3 21V10M9 21V4M15 21v-8M21 21V8" /></svg>
                    <span>{{ $views }}</span>
                </span>
            @endif
            <button type="button" @click="bookmarked = !bookmarked; $dispatch('twitter-card:bookmark', { on: bookmarked })" x-bind:aria-pressed="bookmarked ? 'true' : 'false'" x-bind:class="bookmarked ? 'text-info-text' : ''" class="{{ $actionButton }} !-me-2 !ms-0 hover:bg-info-soft hover:text-info-text" aria-label="{{ __('Bookmark') }}">
                <svg aria-hidden="true" viewBox="0 0 24 24" x-bind:fill="bookmarked ? 'currentColor' : 'none'" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
            </button>
        @else
            <span class="inline-flex items-center gap-2"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5Z" /></svg>{{ $replies }}</span>
            <span class="inline-flex items-center gap-2"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M17 1l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3" /></svg>{{ $retweets }}</span>
            <span class="inline-flex items-center gap-2"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1 7.8 7.8 7.8-7.8 1-1a5.5 5.5 0 0 0 0-7.8Z" /></svg>{{ $likes }}</span>
            @if ($views)
                <span class="inline-flex items-center gap-2"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4"><path d="M3 21V10M9 21V4M15 21v-8M21 21V8" /></svg>{{ $views }}</span>
            @endif
        @endif
    </footer>
</article>

Ownership & lifecycle

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