You've got an orders table in a Filament panel and someone asks for total revenue at the bottom. The usual reflex is a second query in the Blade view, or a widget bolted above the table. Filament table summarizers do it in one method call on the column itself.
The problem: table totals without a second query#
The footer-total pattern people reach for is a separate aggregate query, wired into a custom Blade view or a stats widget sitting above the table. It works until the table gains filters, search or sorting — then the widget shows the total for everything while the table shows a filtered subset, and the two numbers disagree.
A summarizer hangs off the column and aggregates the table's current query. Apply a filter and the total moves with it. The same holds for search and any custom table filters the user has toggled — the summary reflects exactly what's on screen, because it runs against the same query the table does.
Add a Sum summarizer#
->summarize() takes a Summarizer object and renders a row beneath the table. Sum adds up the column's values:
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('total')
->money('gbp')
->summarize(Sum::make()),
Format the summary the same way you format the column, or the total renders as a raw number while the cells show currency:
TextColumn::make('total')
->money('gbp')
->summarize(Sum::make()->money('gbp')),
If you store money as integer pence (the sane way to store money), divideBy scales it back before formatting:
// Column stores 1999 → summary renders £19.99
->summarize(Sum::make()->money('gbp', divideBy: 100)),
Average, Count and Range summarizers#
The other three built-ins live in the same namespace. Average divides the total by the row count:
use Filament\Tables\Columns\Summarizers\Average;
TextColumn::make('rating')
->summarize(Average::make()->numeric(decimalPlaces: 1)),
Count returns the number of values. On its own it just counts rows, so it's most useful scoped to a condition — here, how many orders are paid:
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\Summarizers\Count;
use Illuminate\Database\Query\Builder;
IconColumn::make('is_paid')
->boolean()
->summarize(
Count::make()->query(fn (Builder $query) => $query->where('is_paid', true)),
),
Range shows the minimum and maximum in one summary:
use Filament\Tables\Columns\Summarizers\Range;
TextColumn::make('total')
->summarize(Range::make()),
For date columns, ->minimalDateTimeDifference() renders the range as human dates rather than raw timestamps, which reads far better on a created_at column.
Stack multiple summarizers on one column#
Pass an array and each summarizer renders on its own line under the column:
TextColumn::make('total')
->numeric()
->summarize([
Sum::make()->label('Total'),
Average::make()->label('Average'),
]),
Label them or the stacked numbers become a guessing game — is the second figure an average, a count, a max? The label removes the ambiguity.
Label and scope a summary#
->label() names the figure:
->summarize(Sum::make()->label('Revenue')),
Don't reach for an empty string to hide a label — that leaves screen readers with a bare number and no context. Filament v5 gives you ->hiddenLabel(), which hides the text visually but keeps it in the accessibility tree:
->summarize(Sum::make()->hiddenLabel()),
The one you'll use most is ->query(). It scopes the dataset the summarizer aggregates over, independent of the rows the column displays — so the column can show every order while the total counts only paid ones:
use Illuminate\Database\Query\Builder;
TextColumn::make('total')
->summarize(
Sum::make()
->label('Paid revenue')
->query(fn (Builder $query) => $query->where('status', 'paid')),
),
Note the import: ->query() hands you an Illuminate\Database\Query\Builder, not the Eloquent builder. If the four built-ins don't cover what you need, drop to a raw Summarizer and return the value yourself from ->using():
use Filament\Tables\Columns\Summarizers\Summarizer;
use Illuminate\Database\Query\Builder;
TextColumn::make('total')
->summarize(
Summarizer::make()
->label('Customers')
->using(fn (Builder $query) => $query->distinct()->count('user_id')),
),
That gives you a distinct customer count — something Count alone won't do. The ->query() closure applies the same scoping idea as a table filter, except it constrains the aggregate rather than the row list.
Page total vs all-pages total#
Here's the behaviour that catches people out. On a paginated table Filament shows two summary lines: one for the current page, and — when there's more than one page — a second for every row in the filtered set. The all-pages figure runs its own aggregate query that ignores the pagination limit, so it's a true grand total, not a sum of the rows you can see.
If your table leans on server-driven pagination and sorting, that distinction matters: the all-pages total still aggregates the full result set, not the 25 rows on the current page.
Filament v5 lets you switch either line off with ->summaries() on the table:
use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->summaries(
pageCondition: false,
allTableCondition: true,
);
}
That hides the per-page line and keeps the grand total — handy for grouped reports where the page subtotal is just noise.
Gotchas and edge cases#
The first column can't hold a summarizer. Filament uses that cell to render the summary section's heading, so a summarizer there is silently ignored. Move the summarized column, or add a leading label column.
Every summarizer is its own aggregate query. Ten summarized columns means ten extra queries on each table render — usually fine, but worth watching on a hot dashboard, especially if you're already tuning query counts with Eloquent strict mode to catch N+1s.
Mind the two Builder imports. ->query() on a summarizer takes Illuminate\Database\Query\Builder, while ->hidden() and ->visible() hand you Illuminate\Database\Eloquent\Builder. Import the wrong one and the closure's type hint won't match at runtime.
Range excludes null values by default. If a null should count toward the minimum, opt in with ->excludeNull(false).
Wrapping up#
Summarizers turn a footer total into one line on the column definition — no second query, no drifting widget. Reach for ->query() when you need a conditional total, and a custom Summarizer when the four built-ins fall short.
If you're building out a reporting panel, the next stops are stats overview widgets with trends and sparklines for the numbers at the top of the dashboard, and the full zero-to-production Filament dashboard guide for tying it all together.
FAQ#
How do I show a total at the bottom of a Filament table column?
Add ->summarize(Sum::make()) to the column. Filament renders a summary row beneath the table with the total for that column. There's no separate query or Blade override — the summarizer aggregates the table's current dataset, filters and search included.
What summarizers does Filament include?
Four, all under the Filament\Tables\Columns\Summarizers namespace: Sum, Average, Count and Range. Sum totals the values, Average divides that total by the row count, Count returns the number of values, and Range shows the minimum and maximum. For anything else you build a custom Summarizer and return your value from ->using().
Can I add multiple summarizers to one column?
Yes. Pass an array to ->summarize([...]) and each summarizer renders on its own line under the column. A common pairing is a Sum labelled "Total" with an Average beneath it. Give each one a ->label() so the stacked figures stay readable.
Does the Filament summary total all pages or just the current page?
Both, by default. Filament shows a summary for the current page and, when the data spans multiple pages, a second line totalling every row in the filtered set. The all-pages figure runs its own aggregate query, so it's the true total rather than a sum of the visible rows. Hide the per-page line with ->summaries(pageCondition: false) if it only adds noise.
How do I summarize only certain rows?
Scope the summarizer's dataset with ->query(). Pass a closure that receives the query builder and add your constraints — for example Count::make()->query(fn (Builder $query) => $query->where('is_paid', true)). Only the matching rows feed the calculation, which is how you total paid orders while the column still lists every order.