Table
Columns
Every column type, and the base API they all share — labels, visibility, authorization, sorting, formatting and inline editing.
On this page
- Column Types
- Concepts
- Shared Column API
- Factory & Identity
- Sorting
- Searching
- Visibility & Toggleability
- Responsive Breakpoints
- Responsive Display Variants
- Value Formatting
- Text Styling
- Width & Alignment
- Icons
- URL (Clickable Cell)
- Copyable
- Tooltip & Description
- Summary (Aggregate Footer)
- Extra HTML Attributes
- Pivot Columns
- State Access
- Custom Rendering (Blade Partials)
Wire Table provides 19 column types. They all share the same base column API for labels, visibility, authorization, sorting, formatting, and inline editing — documented below. Pick a type for its cell rendering; reach for the shared API on any of them.
Column Types
| Column | Use for |
|---|---|
| TextColumn | General-purpose text with date/money/number formatting presets |
| BadgeColumn | Status pills with color and icon, incl. enum self-coloring |
| MoneyColumn | Amounts, right-aligned and tabular; the stacked card's metric |
| MetricColumn | A measurement: aggregate figure with an optional trend line |
| PhoneColumn | A phone number, written to be read and linked to be dialled |
| BooleanColumn | True/false as an icon (check / cross) |
| IconColumn | State-based or dynamically resolved icons |
| ImageColumn | Avatars and thumbnails |
| ButtonColumn | Link or Livewire-action button in a cell |
| ToggleColumn | Inline editable on/off switch |
| CheckboxColumn | Inline editable checkbox (a denser ToggleColumn) |
| SelectColumn | Inline editable dropdown (options, relations, enums) |
| TextInputColumn | Inline editable text/number/email input |
| StackedColumn | Avatar + name + email stacked layouts |
| SplitColumn | Compose several columns side by side |
| PollColumn | Live-polling status/progress cells |
| ColorColumn | A stored CSS color as a swatch |
| RatingColumn | A numeric score as stars |
| TagsColumn | A multi-value state as chips |
Concepts
- Relation Paths & Dot Notation — display related-model values, aggregates, pivots
- Enum & JSON Casts — enum labels/colors/icons and array/json rendering
- Editing & Column-Level Filters — inline editing and per-column filter inputs
- Fill Handle — Excel-style drag-to-fill across rows, in one request
- Patterns & Recipes — full example tables
Shared Column API
Every column inherits these capabilities from the base Column class.
Factory & Identity
Column::make(string $name) // static factory — $name is dot-notation path->label(string|Closure $label) // display label in <th> (auto-generated from name)->getName(): string // get column name->getLabel(): string // get resolved label
Sorting
->sortable(bool $sortable = true, ?Closure $query = null)->isSortable(): bool->getSortColumn(): ?string // the attribute the header orders by // Custom sort logic->sortUsing(Closure $fn)
isSortable() decides whether the header is clickable; getSortColumn() decides
what the click orders by. For an ordinary column the two are the same string — its
own name, dotted relation path included — so you never call it. A composite
column is where they part: a SplitColumn is registered under a name for the
group it draws, and answers with the first sortable child it holds. The query
seam asks the column rather than reusing the clicked name, which is what keeps a
composite header from ordering by an attribute that does not exist.
TextColumn::make('full_name') ->sortable() ->sortUsing(function (Builder $query, string $direction) { $query->orderBy('last_name', $direction) ->orderBy('first_name', $direction); })
Searching
->searchable(bool|array $searchable = true)->isSearchable(): bool // Pass an array to search specific DB columns (when the column name is virtual)->searchable(['first_name', 'last_name', 'email']) // Custom search logic->searchUsing(Closure $fn) // Declare what the column holds, so >100 and 10..20 can be typed into search->searchAs(SearchValueType|string $type) // 'text' | 'numeric' | 'date' | 'code' // Get resolved search columns->getSearchColumns(): array
searchColumns(array $columns)as a separate setter exists only onStackedColumn. On other columns, pass the array straight tosearchable().
// Search across multiple DB columnsTextColumn::make('user') ->searchable(['first_name', 'last_name', 'email']) // Custom search logicTextColumn::make('full_name') ->searchable() ->searchUsing(function (Builder $query, string $search) { $query->where(DB::raw("CONCAT(first_name, ' ', last_name)"), 'like', "%{$search}%"); })
searchAs() matters only once the table opts into
range search. The value type is normally
inferred from the model's casts — a decimal:2 or datetime cast is enough —
so declare it only where the casts cannot speak for the column:
// The model has no cast for `amount`, so nothing can be inferred from it.TextColumn::make('amount') ->searchable() ->searchAs('numeric') // now ">1000" and "10..20" reach this column
A column left as text is skipped by a comparison rather than compared lexically, so a wrong or missing declaration narrows what search understands — it never returns wrong rows.
The declaration alone switches nothing on. A searchable column that declares a
type while the table's search does not read ranges is refused when the table
renders, naming the call it is missing — the alternative is a table that comes
back empty because 10..20 was looked for as literal text.
'code' is the one type that is never inferred: it says the value is a series
plus a zero-padded number (8866 01, 8866 02), which is what makes
comparing it as text correct, and only the owner knows that. It unlocks
ranges inside a series —
8866 01..08.
Visibility & Toggleability
->hidden(bool|Closure $hidden = true) // hide column->isHidden(): bool // User-toggleable (column picker)->toggleable(bool $toggleable = true) // Permission-based->permission(?string $permission) // visible only if user has permission->visible(Closure $callback) // custom visibility callback (Closure only) // Per-record cell visibility (redact a single cell by row)->visibleForRecord(Closure $callback) // fn ($record) => bool
->hidden(), ->permission(), ->visible() and ->authorize() decide whether
the column exists in the table at all — they are evaluated once, without a
record (they also drive the header, column toggle and export). To hide or
redact a single cell per row — e.g. show salary only for records the user
may see — use ->visibleForRecord(fn ($record) => …), which runs at cell render
with the row's record. A hidden cell renders empty; the column still occupies its
place in every other row.
TextColumn::make('salary') ->visibleForRecord(fn ($record) => auth()->user()->can('viewSalary', $record));
Responsive Breakpoints
->visibleFrom(string $breakpoint) // hidden below this breakpoint->hiddenFrom(string $breakpoint) // hidden from this breakpoint up->onlyOnMobile() // visible only on mobile (<md)->onlyOnDesktop() // visible only on desktop (≥lg)->onlyOnTabletAndUp() // visible from md up->onlyOnLargeScreens() // visible from xl up
TextColumn::make('phone') ->visibleFrom('md') // hidden on mobile, visible from md TextColumn::make('notes') ->onlyOnLargeScreens() // only visible on xl+
Responsive Display Variants
// Custom render for mobile vs desktop->mobileDisplayUsing(Closure $fn)->desktopDisplayUsing(Closure $fn)->hasResponsiveDisplay(): bool // Where the column lands on a stacked mobile card (see Advanced → Responsive Layout)->mobileTitle() ->mobileSubtitle() ->mobileMetric() ->mobileMeta() ->mobileDetail()
TextColumn::make('user') ->mobileDisplayUsing(fn ($record) => $record->name) ->desktopDisplayUsing(fn ($record) => "{$record->name} <{$record->email}>")
Value Formatting
->formatStateUsing(Closure $fn) // transform value for display->displayUsing(Closure $fn) // alias for formatStateUsing->default(mixed $value) // value when state is null->placeholder(string $text) // text shown when value is null/empty->limit(int $chars) // truncate to N characters->prefix(string $prefix) // prepend text->suffix(string $suffix) // append text->html(bool $html = true) // render value as raw HTML->wrap(bool $wrap = true) // allow text wrapping (default: nowrap)
TextColumn::make('price') ->prefix("$") ->suffix(' USD') ->placeholder('N/A') TextColumn::make('bio') ->limit(100) ->tooltip(fn ($record) => $record->bio) // show full on hover TextColumn::make('content') ->html() ->wrap() ->limit(200)
Text Styling
Use ->textSize() for the cell's font size. ->size() (from the shared HasSize concern) sets the column's structural size and does not change the text font.
->textSize(string $size) // 'xs', 'sm', 'md', 'lg', 'xl' — text font size->weight(string $weight) // 'thin', 'light', 'normal', 'medium', 'semibold', 'bold', 'extrabold'->textColor(string $color) // Tailwind color name or 'gray', 'primary', etc.->fontFamily(string $family) // 'sans', 'serif', 'mono' (TextColumn only)
TextColumn::make('name') ->weight('bold') ->textSize('lg') TextColumn::make('subtitle') ->textSize('sm') ->textColor('gray') ->weight('light')
Width & Alignment
->width(string $width) // CSS width: '200px', '20%', 'auto'->alignment(string $alignment) // 'left', 'center', 'right'->alignLeft() // shortcut->alignCenter() // shortcut->alignRight() // shortcut
Icons
->icon(string|Icon|Closure|null $icon, ?string $position = 'before') // position: 'before' | 'after'->color(string|Color $color) // the column's colour: text, and the icon when it has no colour of its own->iconColor(string|Color|Closure|null $color) // the icon's colour, per record — a role, or a closure returning one->iconTile(bool $tile = true) // seat the icon in a tinted tile — the list archetype's row anchor
On a list (layout('list')) reach for iconTile() as well: a bare tinted
glyph is enough on a grid of columns, where the row is already a line of aligned
values, but where the record is a sentence the tile is what gives the rows a left
edge for the eye to run down. Ground and ink come from the one role, so they
cannot drift apart, and only the semantic roles get a tile — a raw hue makes no
statement about kind, so it lands on the neutral one.
color() is resolved once for the whole column, which is right for a text tint
and wrong for a status icon whose whole job is to differ per row. That is
what iconColor() is for: pass a role from the shared vocabulary, or a closure
over the record. A closure gives up the column's static icon memo — the same
cost a closure icon() already pays, and the reason neither is the default.
TextColumn::make('state') ->icon(fn ($record) => $record->failed ? 'x-circle' : 'check-circle') ->iconColor(fn ($record) => $record->failed ? 'danger' : 'success') TextColumn::make('email') ->icon('mail', 'before') ->color('primary')
URL (Clickable Cell)
->actionUrl(Closure $url, bool $openInNewTab = false) // make the cell a link
TextColumn::make('name') ->actionUrl(fn ($record) => route('users.show', $record), openInNewTab: true) ->color('primary')
Copyable
->copyable(bool $copyable = true) // click-to-copy icon->copyMessage(string $msg) // feedback text after copy
Tooltip & Description
->tooltip(string|Closure $tooltip) // hover tooltip->description(string|Closure $desc) // secondary text below value
TextColumn::make('title') ->description(fn ($record) => Str::limit($record->body, 50)) ->tooltip(fn ($record) => "Created: {$record->created_at->format('d.m.Y')}")
Summary (Aggregate Footer)
->summarize(string $aggregate, ?string $label = null)
Available aggregates: 'sum', 'avg', 'count', 'min', 'max', 'range'
See Advanced — Summary for details.
Extra HTML Attributes
->extraAttributes(array $attrs) // on <td>->extraHeaderAttributes(array $attrs) // on <th>
TextColumn::make('notes') ->extraAttributes(['data-testid' => 'notes-cell']) ->extraHeaderAttributes(['class' => 'bg-gray-100'])
Pivot Columns
->pivot(bool $isPivot = true) // marks as pivot table column->isPivot(): bool
For many-to-many relationships with pivot data:
TextColumn::make('roles.pivot.assigned_at') ->pivot() ->dateTime('d.m.Y')
State Access
->state(mixed $value) // override state value->getState(Model $record): mixed // resolve state from record
Custom Rendering (Blade Partials)
Every column owns its state/configuration and delegates markup to a Blade
partial under packages/table/resources/views/tables/columns/. The base text
cell renders through text.blade.php; each custom-UI column has its own partial
(badge, boolean, icon, image, button, toggle, poll, split,
stacked, select, text-input-*). Columns never return inline HTML from
renderCell() — they call renderView('tables.columns.<name>', [...]).
Two ways to customize the markup:
// 1. Per-column override — point any column at your own Blade view.TextColumn::make('name')->view('columns.my-name-cell'); // 2. Project-wide override — publish the package views and edit the partial.// php artisan vendor:publish --tag=wire-table::views// then edit resources/views/vendor/wire-table/tables/columns/badge.blade.php
View resolution order: an explicit ->view() wins, then the package view
(wire-table::tables.columns.<name>), then an app-level view of the same name.
Your partial receives exactly the data the built-in one does — the already
resolved state/config primitives for that column — so you only rewrite the HTML.