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.
Preview
<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
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.
-
resources/views/components/ui/twitter-card.blade.php
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.
# 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
Props
| 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.
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
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
- Theming hooks
Accessibility
- 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-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, 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 underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<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.
<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.
@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