K

Table

Columns

Every column type, and the base API they all share — labels, visibility, authorization, sorting, formatting and inline editing.

On this page

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

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 on StackedColumn. On other columns, pass the array straight to searchable().

// Search across multiple DB columns
TextColumn::make('user')
->searchable(['first_name', 'last_name', 'email'])
 
// Custom search logic
TextColumn::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 series8866 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')}")
->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.