File Thumbnail
Compact tile that previews a file by type with an icon, extension badge and filename caption.
Preview
<div class="mx-auto max-w-lg">
<div class="grid grid-cols-4 gap-6 sm:grid-cols-6">
<x-ui.file-thumbnail type="pdf" name="invoice.pdf" />
<x-ui.file-thumbnail type="docx" name="proposal.docx" />
<x-ui.file-thumbnail type="xlsx" name="budget-2026.xlsx" />
<x-ui.file-thumbnail type="pptx" name="pitch.pptx" />
<x-ui.file-thumbnail type="csv" name="export.csv" />
<x-ui.file-thumbnail
type="image"
name="cover.jpg"
src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?auto=format&fit=crop&w=200&q=60"
/>
<x-ui.file-thumbnail type="zip" name="assets.zip" />
<x-ui.file-thumbnail type="mp3" name="intro.mp3" />
<x-ui.file-thumbnail type="mp4" name="demo-recording.mp4" />
<x-ui.file-thumbnail type="json" name="config.json" />
<x-ui.file-thumbnail type="txt" name="notes.txt" />
<x-ui.file-thumbnail type="generic" name="unknown.bin" />
</div>
</div>
Installation
php artisan ui:add file-thumbnail
Registry contract
php artisan ui:add file-thumbnail
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/file-thumbnail.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
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: File Thumbnail (`file-thumbnail`)
Compact tile that previews a file by type with an icon, extension badge and filename caption.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add file-thumbnail
```
## Usage
```blade
<div class="mx-auto max-w-lg">
<div class="grid grid-cols-4 gap-6 sm:grid-cols-6">
<x-ui.file-thumbnail type="pdf" name="invoice.pdf" />
<x-ui.file-thumbnail type="docx" name="proposal.docx" />
<x-ui.file-thumbnail type="xlsx" name="budget-2026.xlsx" />
<x-ui.file-thumbnail type="pptx" name="pitch.pptx" />
<x-ui.file-thumbnail type="csv" name="export.csv" />
<x-ui.file-thumbnail
type="image"
name="cover.jpg"
src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?auto=format&fit=crop&w=200&q=60"
/>
<x-ui.file-thumbnail type="zip" name="assets.zip" />
<x-ui.file-thumbnail type="mp3" name="intro.mp3" />
<x-ui.file-thumbnail type="mp4" name="demo-recording.mp4" />
<x-ui.file-thumbnail type="json" name="config.json" />
<x-ui.file-thumbnail type="txt" name="notes.txt" />
<x-ui.file-thumbnail type="generic" name="unknown.bin" />
</div>
</div>
```
## Props
- `type` (string, default `generic`) — File extension or family used to choose the icon glyph and tone, such as pdf, docx, xlsx, zip, mp3, mp4, or an image extension.
- `name` (mixed|null, default `null`) — Filename shown in the caption and used as the tile's accessible label; the extension is kept from truncating even when the base name is long.
- `src` (mixed|null, default `null`) — Image URL; when type is an image family and src is set, renders an actual thumbnail instead of the generic page glyph.
- `size` (sm|lg, default `md`) — Tile size: sm, md, or lg.
- `caption` (bool, default `true`) — Shows the filename caption under the tile.
## Use when
- Use when users need to upload, inspect, edit, annotate, or sign real documents as part of the workflow.
- Representing an uploaded or attached file in a grid or list before it is opened, such as in a file picker or attachments panel.
- Needing a type-appropriate icon and extension tag without loading the actual file content.
## Avoid when
- Do not use a heavy document interface when the same task can be completed more simply in structured product UI.
- The file's content itself should be previewed inline; use pdf-viewer, docx-viewer, excel-viewer, or csv-viewer for that file type.
- Listing many files in a dense, sortable table; use table with a smaller icon instead.
## Anti-patterns
- Heavy document UI when structured fields are simpler
## Rules
- Use the `<brok:file-thumbnail>` tag (or `<x-ui.file-thumbnail>`) 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/file-thumbnail
- Registry JSON (files, props, contract): https://brokui.dev/r/open/file-thumbnail.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 |
|---|---|---|---|
| type | string | generic | File extension or family used to choose the icon glyph and tone, such as pdf, docx, xlsx, zip, mp3, mp4, or an image extension. |
| name | mixed | null | null | Filename shown in the caption and used as the tile's accessible label; the extension is kept from truncating even when the base name is long. |
| src | mixed | null | null | Image URL; when type is an image family and src is set, renders an actual thumbnail instead of the generic page glyph. |
| size | sm | lg | md | Tile size: sm, md, or lg. |
| caption | bool | true | Shows the filename caption under the tile. |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Maps the type prop to a file family (document, spreadsheet, slides, archive, audio, video, code, image, or generic), each with its own glyph and token-bound tone.
- For image types with a src, renders an actual cropped thumbnail instead of the page-silhouette glyph.
- The caption keeps the file extension from truncating, so a long base name shortens while ".xlsx" or similar stays visible.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Upload, inspect, edit, annotate, or sign a real document.
Use when
- Use when users need to upload, inspect, edit, annotate, or sign real documents as part of the workflow.
- Representing an uploaded or attached file in a grid or list before it is opened, such as in a file picker or attachments panel.
- Needing a type-appropriate icon and extension tag without loading the actual file content.
Avoid when
- Do not use a heavy document interface when the same task can be completed more simply in structured product UI.
- The file's content itself should be previewed inline; use pdf-viewer, docx-viewer, excel-viewer, or csv-viewer for that file type.
- Listing many files in a dense, sortable table; use table with a smaller icon instead.
Use instead
- Structured product form
- Direct data entry
Anti-patterns
- Heavy document UI when structured fields are simpler
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The tile carries role="img" with an aria-label built from the filename, so the file type and name are announced as one unit rather than through a decorative icon plus separate caption text.
- 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
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'type' => 'generic',
'name' => null,
'src' => null,
'size' => 'md',
'caption' => true,
])
@php
$type = strtolower(trim((string) $type));
$imageTypes = ['image', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif'];
$isImage = in_array($type, $imageTypes, true);
// type → family: the page's tint + tag colour and its content glyph. Each
// family uses one status/chart token so the tile stays theme-bound.
$families = [
'pdf' => ['pdf', 'destructive'],
'doc' => ['document', 'info'], 'docx' => ['document', 'info'], 'txt' => ['document', 'muted'], 'rtf' => ['document', 'info'], 'md' => ['document', 'muted'],
'xls' => ['spreadsheet', 'success'], 'xlsx' => ['spreadsheet', 'success'], 'csv' => ['spreadsheet', 'success'], 'numbers' => ['spreadsheet', 'success'],
'ppt' => ['slides', 'warning'], 'pptx' => ['slides', 'warning'], 'key' => ['slides', 'warning'],
'zip' => ['archive', 'warning'], 'rar' => ['archive', 'warning'], '7z' => ['archive', 'warning'], 'tar' => ['archive', 'warning'], 'gz' => ['archive', 'warning'],
'mp3' => ['audio', 'chart-4'], 'wav' => ['audio', 'chart-4'], 'm4a' => ['audio', 'chart-4'],
'mp4' => ['video', 'chart-4'], 'mov' => ['video', 'chart-4'], 'webm' => ['video', 'chart-4'],
'js' => ['code', 'chart-1'], 'ts' => ['code', 'chart-1'], 'php' => ['code', 'chart-1'], 'json' => ['code', 'chart-1'], 'html' => ['code', 'chart-1'], 'css' => ['code', 'chart-1'],
];
foreach ($imageTypes as $it) {
$families[$it] = ['image', 'chart-2'];
}
[$glyph, $tone] = $families[$type] ?? ['generic', 'muted'];
// Fixed utilities per tone (JIT-visible; never spliced).
$tones = [
'destructive' => ['page' => 'fill-destructive/10 stroke-destructive/40', 'glyph' => 'text-destructive', 'tag' => 'bg-destructive text-destructive-foreground'],
'info' => ['page' => 'fill-info/10 stroke-info/40', 'glyph' => 'text-info', 'tag' => 'bg-info text-primary-foreground'],
'success' => ['page' => 'fill-success/10 stroke-success/40', 'glyph' => 'text-success', 'tag' => 'bg-success text-primary-foreground'],
'warning' => ['page' => 'fill-warning/10 stroke-warning/40', 'glyph' => 'text-warning', 'tag' => 'bg-warning text-primary-foreground'],
'chart-1' => ['page' => 'fill-chart-1/10 stroke-chart-1/40', 'glyph' => 'text-chart-1', 'tag' => 'bg-chart-1 text-primary-foreground'],
'chart-2' => ['page' => 'fill-chart-2/10 stroke-chart-2/40', 'glyph' => 'text-chart-2', 'tag' => 'bg-chart-2 text-primary-foreground'],
'chart-4' => ['page' => 'fill-chart-4/10 stroke-chart-4/40', 'glyph' => 'text-chart-4', 'tag' => 'bg-chart-4 text-primary-foreground'],
'muted' => ['page' => 'fill-muted stroke-border', 'glyph' => 'text-muted-foreground', 'tag' => 'bg-muted-foreground text-primary-foreground'],
];
$t = $tones[$tone];
$tileSize = match ($size) {
'sm' => 'size-10',
'lg' => 'size-20',
default => 'size-14',
};
$tagSize = match ($size) {
'sm' => 'px-1 text-[8px] leading-3',
'lg' => 'px-2 text-xs leading-5',
default => 'px-1 text-2xs leading-4',
};
$captionWidth = match ($size) {
'sm' => 'max-w-20',
'lg' => 'max-w-32',
default => 'max-w-24',
};
$extension = $type !== '' && $type !== 'generic' && ! $isImage ? strtoupper($type) : null;
// Caption keeps the extension visible: the base name truncates, the
// extension never does ("budget-2026….xlsx" rather than "budget-…").
$dot = $name !== null ? strrpos($name, '.') : false;
$base = $dot !== false && $dot > 0 ? substr($name, 0, $dot) : (string) $name;
$ext = $dot !== false && $dot > 0 ? substr($name, $dot) : '';
$label = $name !== null && $name !== ''
? $name
: __(':type file', ['type' => strtoupper($type !== '' ? $type : 'generic')]);
@endphp
<div
data-slot="file-thumbnail"
data-type="{{ $type }}"
data-size="{{ $size }}"
{{ $attributes->merge(['class' => 'inline-flex flex-col items-center gap-2 text-center']) }}
>
<div
role="img"
aria-label="{{ $label }}"
class="relative flex shrink-0 items-center justify-center {{ $tileSize }}"
>
@if ($isImage && $src)
{{-- The wrapping container carries role="img" + aria-label, so the
image itself is decorative to avoid a double announcement. --}}
<img
src="{{ $src }}"
alt=""
aria-hidden="true"
loading="lazy"
class="size-full rounded-lg border border-border object-cover"
/>
@else
{{-- A page silhouette with a folded corner fills the tile; the type
glyph sits in the page, the extension tag overlaps its bottom
edge so it never covers the glyph. --}}
<svg viewBox="0 0 40 48" class="h-full w-auto {{ $t['glyph'] }}" aria-hidden="true">
<path d="M6 1.5h20l8 8V44a2.5 2.5 0 0 1-2.5 2.5h-25A2.5 2.5 0 0 1 4 44V4a2.5 2.5 0 0 1 2.5-2.5z" class="{{ $t['page'] }}" stroke-width="1.25" stroke-linejoin="round" />
<path d="M26 1.5v6a2 2 0 0 0 2 2h6" class="{{ $t['page'] }}" stroke-width="1.25" stroke-linejoin="round" fill="none" />
<g fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" transform="translate(11 16) scale(0.75)">
@switch($glyph)
@case('pdf')
<path d="M3 4h18M3 9h18M3 14h10M3 19h6" />
@break
@case('document')
<path d="M3 5h18M3 10h18M3 15h12" />
@break
@case('spreadsheet')
<path d="M3 4h18v14H3zM3 9h18M3 13.5h18M10 4v14M16 4v14" />
@break
@case('slides')
<path d="M3 4h18v12H3zM12 16v4M8 20h8" />
@break
@case('archive')
<path d="M10 3v2m0 2v2m0 2v2M8 13h4v6H8z" />
@break
@case('audio')
<path d="M9 18V6l11-2v12M6 18a3 3 0 1 0 6 0 3 3 0 0 0-6 0zM17 16a3 3 0 1 0 6 0 3 3 0 0 0-6 0z" />
@break
@case('video')
<path d="M3 5h13v14H3zM16 10l6-3v10l-6-3z" />
@break
@case('code')
<path d="M8 6 2 12l6 6M16 6l6 6-6 6" />
@break
@case('image')
<path d="M3 4h18v16H3zM8 10a2 2 0 1 0 0-.01M21 16l-5-5-9 9" />
@break
@default
<path d="M6 12h12M12 6v12" class="opacity-0" />
@endswitch
</g>
</svg>
@if ($extension)
<span class="absolute -bottom-1 start-0 rounded-sm font-semibold uppercase tracking-wide shadow-sm {{ $tagSize }} {{ $t['tag'] }}">
{{ $extension }}
</span>
@endif
@endif
</div>
@if ($caption && $name !== null && $name !== '')
<span class="flex text-xs text-muted-foreground {{ $captionWidth }}" title="{{ $name }}">
<span class="min-w-0 truncate">{{ $base }}</span><span class="shrink-0">{{ $ext }}</span>
</span>
@endif
</div>
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