Add Conditional Formatting to FilamentPHP Table Columns in Minutes
Learn how to highlight rows in a FilamentPHP resource table—e.g., red ‘Overdue’ status—using the Column::formatStateUsing callback. The guide shows a full code example, explains performance limits, and lists common mistakes to avoid.
03 Sept 2025, 14:11 UTC

Conditional Formatting in FilamentPHP Table Columns
Most admin panels need to draw attention to specific records – for example, overdue invoices, pending approvals, or low stock items. FilamentPHP makes this trivial by letting you inject custom HTML or Blade views into a column via formatStateUsing. The result is a single line of code that changes the cell’s appearance based on its value.
Why Use It?
Highlighting status values improves readability and reduces the time a user spends scanning a list. Unlike CSS alone, the logic lives in the PHP layer, so you can base the style on database values, relationships, or computed attributes.
How It Works
In Filament 3, a resource’s table columns are defined in public static function getTableColumns(): array. Each column is an instance of Filament\Tables\Columns\Column (or a subclass). The formatStateUsing method accepts a callback that receives the column’s raw state and the Eloquent model instance for the row. Returning a string will be escaped automatically; returning a Markup instance or a Blade view will be rendered as raw HTML.
Implementing a Conditional Column
Basic Example
Suppose you have a posts table with a status column that can be published, draft, or overdue. To display overdue in red, add the following to app/Filament/Resources/PostResource.php:
use Filament\Tables\Columns\Column;
use Filament\Tables\Tables;
use Illuminate\Support\HtmlString;
class PostResource extends Resource
{
// ... existing code ...
public static function getTableColumns(): array
{
return [
Column::make('status')
->label('Status')
// The callback receives the raw state and the record.
->formatStateUsing(fn ($state, $record) =>
$state === 'overdue'
? new HtmlString('Overdue')
: $state
),
];
}
}
Key points:
- Use
HtmlString(orIlluminate\Support\HtmlString) to tell Filament that the string is safe and should not be double‑escaped. - Only the
statuscolumn is styled; other statuses appear normally. - Because the callback runs once per row after the Eloquent query, it is inexpensive.
Using Blade Views
If you prefer Blade syntax or need more complex markup, return a rendered view:
->formatStateUsing(fn ($state, $record) =>
view('components.status-badge', [
'state' => $state,
'record' => $record,
])->render()
);
In resources/views/components/status-badge.blade.php you might have:
@props(['state'])
$state === 'overdue',
'text-green-600' => $state === 'published',
'text-gray-500' => $state === 'draft',
])>{{ $state }}
Adding Sortability
By default, formatStateUsing does not affect sorting. If you want the column to be sortable, supply a sortUsing callback so Filament knows how to order the underlying query:
Column::make('status')
->sortable()
->sortUsing(fn ($query, $direction) =>
$query->orderBy('status', $direction)
)
->formatStateUsing(...);
Without sortUsing, clicking the header will display a warning that the column cannot be sorted.
Performance & Security Considerations
- Keep callbacks light. The closure runs once per row on the index page. Heavy database queries or CPU‑intensive calculations will slow down the entire table.
- Use eager loading. If you reference relationships inside the callback, add
->with('relationship')to the resource’sgetTableQuery()method to avoid the N+1 problem. - Escape output. Returning raw HTML without
HtmlStringorMarkup::rawcan expose the admin UI to XSS if the state contains user input. Always sanitize or mark safe explicitly. - Avoid state mutation. The callback should not alter the model or persist changes; it only formats the display.
Common Pitfalls
- Returning unescaped HTML. Filament will escape strings by default, turning
<span>into text. Wrap the string inHtmlStringorMarkup::raw. - Assuming the callback receives the full Eloquent model. It receives the record, but relationships may be missing unless eager‑loaded.
- Using the same closure for multiple columns without cloning the
Columninstance can lead to unexpected state sharing. - Forgetting to add the
->label()method, which makes the header readable.
Verifying the Result
After adding the code, run your application and navigate to the resource index (e.g., /admin/posts). Rows with status = 'overdue' should display the word Overdue in bold red text. If you are using the Blade view, confirm that the CSS classes are applied correctly.
Optional test: create a feature test that seeds an overdue post and asserts that the rendered HTML contains text-red-600:
public function test_overdue_status_is_red()
{
$post = Post::factory()->create(['status' => 'overdue']);
$response = $this->get('/admin/posts');
$response->assertSeeInOrder([
'Overdue',
], false);
}
When to Avoid Heavy Logic
If your formatting requires complex calculations or external API calls, move that logic into a computed attribute on the model or a dedicated presenter service. Then simply reference the computed value in formatStateUsing – the closure will remain fast.
In summary, formatStateUsing gives you a clean, declarative way to add visual cues to Filament tables. Keep the closure lightweight, sanitize output, and remember that sorting is optional and must be explicitly defined.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.