Card
A content container with header, content and footer sections; renders as div, a, article, section, li, details, dl or figure, with a frameless ghost variant.
Preview
Billing
Manage your subscription and invoices.
Your plan renews on the 1st of each month.
{{-- card.title is an h3 by default; `as` sets the level that fits the page outline (h2-h6, p or div). --}}
<x-ui.card class="w-full max-w-sm text-start">
<x-ui.card.header>
<x-ui.card.title as="h2">{{ __('Billing') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Manage your subscription and invoices.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
<p class="text-sm text-muted-foreground">{{ __('Your plan renews on the 1st of each month.') }}</p>
</x-ui.card.content>
<x-ui.card.footer>
<x-ui.button class="w-full">{{ __('Update plan') }}</x-ui.button>
</x-ui.card.footer>
</x-ui.card>
Installation
php artisan ui:add card
Registry contract
php artisan ui:add 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/card.blade.php -
resources/views/components/ui/card/header.blade.php -
resources/views/components/ui/card/title.blade.php -
resources/views/components/ui/card/description.blade.php -
resources/views/components/ui/card/content.blade.php -
resources/views/components/ui/card/footer.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: Card (`card`)
A content container with header, content and footer sections; renders as div, a, article, section, li, details, dl or figure, with a frameless ghost variant.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add card
```
## Usage
```blade
{{-- card.title is an h3 by default; `as` sets the level that fits the page outline (h2-h6, p or div). --}}
<x-ui.card class="w-full max-w-sm text-start">
<x-ui.card.header>
<x-ui.card.title as="h2">{{ __('Billing') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Manage your subscription and invoices.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
<p class="text-sm text-muted-foreground">{{ __('Your plan renews on the 1st of each month.') }}</p>
</x-ui.card.content>
<x-ui.card.footer>
<x-ui.button class="w-full">{{ __('Update plan') }}</x-ui.button>
</x-ui.card.footer>
</x-ui.card>
```
## Props
- `as` (string, default `div`) — Element the card renders as: div, article, section, li, details, dl, or figure; ignored (forced to a) when href is set.
- `href` (mixed|null, default `null`) — Renders the card as a link and adds a hover lift, while keeping the same surface recipe.
- `variant` (string, default `default`) — default is the bordered, filled surface; ghost has no frame, fill, or shadow, so only the layout parts sit on the page background.
## Use when
- Use to group related content into scannable units for browsing, prioritization, or rearrangement.
- Grouping related content (a title, description, body, and actions) into one bordered, elevated surface.
- Building a clickable card that links to a whole record or page via href.
## Avoid when
- Do not use card-like structures when dense row-by-row comparison would be clearer in a list or table.
- The content needs no visual framing, just spacing; use stack or a plain surface instead.
- The composition wants layout parts without a bordered surface at all; use variant="ghost" rather than reaching for a different primitive.
## Anti-patterns
- Turning every object into a card
## Rules
- Use the `<brok:card>` tag (or `<x-ui.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/card
- Registry JSON (files, props, contract): https://brokui.dev/r/open/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 |
|---|---|---|---|
| as | string | div | Element the card renders as: div, article, section, li, details, dl, or figure; ignored (forced to a) when href is set. |
| href | mixed | null | null | Renders the card as a link and adds a hover lift, while keeping the same surface recipe. |
| variant | string | default | default is the bordered, filled surface; ghost has no frame, fill, or shadow, so only the layout parts sit on the page background. |
Slots
default— Compositional parts: header (with title and description), content, and footer.x-ui.card.header— Installed subcomponent from the registry item.x-ui.card.title— Installed subcomponent from the registry item.x-ui.card.description— Installed subcomponent from the registry item.x-ui.card.content— Installed subcomponent from the registry item.x-ui.card.footer— Installed subcomponent from the registry item.
Renders as div, a, article, section, li, details, dl, figure.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- A card with href renders as a real anchor and gains a hover lift, so the whole surface is clickable and keyboard-focusable.
- Components built on top of the card (such as current-plan or an approval workflow) can override data-slot and data-variant to present under their own name while reusing the same recipe.
- Structural primitive only; all behavior comes from composed children.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Organize browsable objects into scannable units.
Use when
- Use to group related content into scannable units for browsing, prioritization, or rearrangement.
- Grouping related content (a title, description, body, and actions) into one bordered, elevated surface.
- Building a clickable card that links to a whole record or page via href.
Avoid when
- Do not use card-like structures when dense row-by-row comparison would be clearer in a list or table.
- The content needs no visual framing, just spacing; use stack or a plain surface instead.
- The composition wants layout parts without a bordered surface at all; use variant="ghost" rather than reaching for a different primitive.
Use instead
- Table for exact comparison
- List for compact scanning
Anti-patterns
- Turning every object into a card
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- A link card is a real anchor, not a div with a click handler, so it is reachable and operable from the keyboard like any other link.
- card.title is an h3 by default; set as to the level that follows the heading above the card so the outline has no skipped or out-of-order levels, or to p/div when the title should not be a heading at all.
- 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 files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
// Element the card renders as: div (default), a (with href), article,
// section, figure. A link card keeps the surface recipe and gains the
// hover lift.
'as' => 'div',
'href' => null,
// default: the bordered surface. ghost: no frame, no fill, no shadow —
// the card's layout parts on the page background, for a composition
// that sometimes wants the surface and sometimes does not.
'variant' => 'default',
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$allowedTags = ['div', 'a', 'article', 'section', 'li', 'details', 'dl', 'figure'];
$tag = $href !== null ? 'a' : (in_array($as, $allowedTags, true) ? $as : 'div');
$variant = $styles['normalizeVariant']($variant);
$variant = $variant === 'ghost' ? 'ghost' : 'default';
$baseClass = $variant === 'ghost' ? $styles['card']['ghost'] : $styles['card']['base'];
// A max-width passed by the caller replaces the base `max-w-full` rather than
// competing with it: Tailwind orders `max-w-2xl`, `max-w-7xl` and arbitrary
// values before `max-w-full`, so the base would otherwise keep the card full width.
if (preg_match('/(?<!\S)max-w-\S+/', (string) $attributes->get('class', ''))) {
$baseClass = trim((string) preg_replace('/(?<!\S)max-w-full(?!\S)/', '', $baseClass));
}
$classes = $baseClass.($tag === 'a' ? ' '.$styles['card']['link'] : '');
// A component built on this card (current-plan, approval-workflow, …)
// passes its own data-slot; the browser keeps only the first of two
// identical attributes, so read the override and print exactly one.
$slotName = (string) ($attributes->get('data-slot') ?: 'card');
// Same rule for data-variant: a composing component's own variant name
// wins over the card's `ghost` marker.
$variantName = (string) ($attributes->get('data-variant') ?: ($variant === 'ghost' ? 'ghost' : ''));
@endphp
<{{ $tag }}
data-slot="{{ $slotName }}"
@if ($variantName !== '') data-variant="{{ $variantName }}" @endif
@if ($tag === 'a') href="{{ $href }}" @endif
{{ $attributes->except(['data-slot', 'data-variant'])->merge(['class' => $classes]) }}
>
{{ $slot }}
</{{ $tag }}>
<div
data-slot="card-header"
{{ $attributes->merge(['class' => 'flex min-w-0 flex-col gap-stack-sm p-card']) }}
>
{{ $slot }}
</div>
@props([
// Element the title renders as: h3 (default), h2, h4, h5, h6, p or div.
// Match the page's heading outline; an unknown value falls back to h3.
'as' => 'h3',
])
@php
$tag = in_array($as, ['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div'], true) ? $as : 'h3';
@endphp
<{{ $tag }}
data-slot="card-title"
{{ $attributes->merge(['class' => 'break-words text-lg font-semibold leading-none tracking-tight']) }}
>
{{ $slot }}
</{{ $tag }}>
<p
data-slot="card-description"
{{ $attributes->merge(['class' => 'break-words text-sm text-muted-foreground']) }}
>
{{ $slot }}
</p>
<div
data-slot="card-content"
{{ $attributes->merge(['class' => 'min-w-0 p-card pt-0']) }}
>
{{ $slot }}
</div>
<div
data-slot="card-footer"
{{ $attributes->merge(['class' => 'flex min-w-0 items-center p-card pt-0']) }}
>
{{ $slot }}
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.4.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