6.3 KiB
Filament Table UX Standard
Canonical rules for all Filament table/list surfaces in TenantPilot. Every new or modified table must follow this standard. Deviations must be documented in the spec or PR with rationale.
Last reviewed: 2026-03-09
Governing Principle
Filament-native first. Table UX standardization uses native Filament features wherever reasonably possible. Custom helpers, macros, traits, or plugins are only allowed when Filament cannot express the pattern cleanly enough inline.
Column Tier Model
Every production table must classify its columns into one of three tiers. This is a review convention, not a code abstraction.
Primary
Always visible. Not toggle-hidden. Anchors the row identity.
- name, display_name, title
- primary status (if truly central)
- core subject identifier
Context
Visible by default. May be toggleable. Provides operational context needed for a normal scan.
- environment, type, category, severity
- key counts / aggregations
- last sync / last activity
- tenant/workspace context where needed
Detail
Hidden by default (toggleable(isToggledHiddenByDefault: true)). Technical or low-frequency information.
- IDs (tenant_id, external_id, entra_id)
- created_at, updated_at
- created_by
- domains, low-frequency secondary statuses
Visible Column Limit
General-purpose tables should expose 7 or fewer columns by default. Denser power-user surfaces may exceed this with documented justification.
Sortability Rules
Mandatory sortable (when present)
- Primary identifier fields (name, display_name, title)
- Status / outcome / enum columns
- All key timestamps used for recency
- Numeric count and size columns
- Version / sequence numbers
Default sort
Every production list must define an explicit defaultSort().
| List type | Default pattern |
|---|---|
| Time-series / logs / runs / alerts | newest first |
| Named entities | name ascending |
| Versioned entities | latest/highest version first |
| Audit / evidence | newest recorded first |
Searchability Rules
- Every non-trivial production table must make its primary identifier
searchable(). - Additional fields only if query-safe and operationally useful.
- Avoid searchable on relations without proper indexing.
- Avoid searchable on computed values without clear SQL handling.
Toggleability Rules
Must be hidden by default
- Technical IDs
- External IDs
- Tenant/workspace internal references (unless primary context)
created_at,updated_at- Secondary technical metadata
Overflow rule
If a table exceeds the calm default surface, lower-value columns must move into Detail via toggleable(isToggledHiddenByDefault: true) before any cosmetic workaround is considered.
Timestamp Standard
Lists
TextColumn::make('created_at')
->since()
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->tooltip(fn ($record) => $record->created_at?->toDateTimeString());
- Primary visual:
->since()for scan-first recency - Absolute date/time available via tooltip
Detail / view pages
- Use absolute
->dateTime()or equivalent explicit formatting.
Exceptions
- Version or evidence tables may use absolute time when exact chronology is the primary task.
Null / Empty Values
->placeholder('—')
Consistent across all tables. Do not mix blank cells, n/a, unknown, or dash variants.
ID Presentation
- Hidden by default
copyable()when operationally relevantlimit()and/ortooltip()for long identifiers- Monospaced styling preferred for technical IDs
- Must not dominate layout width
Status / Badge / Boolean
- Status and enum columns use native
badge()via centralizedBadgeCatalog/BadgeRenderer(per BADGE-001). - Booleans use native icon or badge-based rendering consistently.
Empty State Standard
Every production table must define a domain-specific empty state:
->emptyStateHeading('No policies found')
->emptyStateDescription('Sync your first tenant to see policies here.')
->emptyStateActions([
// exactly 1 primary CTA, RBAC-gated
])
Pagination Profiles
Implementation: App\Support\Filament\TablePaginationProfiles
| Surface | Page sizes | Default |
|---|---|---|
| Resource | 25, 50, 100 |
25 |
| Relation manager | 10, 25, 50 |
10 |
| Widget | 10 |
10 |
| Picker | 25, 50, 100 |
25 |
| Custom page | 25, 50, all |
25 unless override documented |
Table State Persistence
Resource list tables in the critical set must use:
->persistFiltersInSession()
->persistSearchInSession()
->persistSortInSession()
Critical persistence set
- TenantResource
- PolicyResource
- BackupSetResource
- BackupScheduleResource
- ProviderConnectionResource
- FindingResource
- OperationRunResource
Documented exceptions
- Dashboard widgets: no persistence (glance surfaces)
- Directory pages: no persistence (computed health metrics)
- Picker tables: no persistence (workflow-local)
Responsive / Overflow Guardrails
- Long text: use
wrap()where appropriate - Long IDs / technical strings: use
limit()andtooltip() - Solve width problems via better default visibility, not manual width hacks
- No
extraAttributes()width tuning as a default pattern - No resize plugin as substitute for better defaults
Performance Guardrails
- No casual
sortable()/searchable()on relation or computed columns without query review - No row-by-row counts or
exists()checks in hot lists unless optimized - Prefer eager loading where relation-backed columns are rendered repeatedly
- No costly default sorts without understanding DB impact
RBAC / Tenancy Safety
- Technical cross-tenant identifiers must not be more prominent than needed
- Empty-state actions and row/header actions remain capability-gated
- Cross-tenant/workspace tables must preserve enough context to avoid ambiguity
- This standard must not change authorization behavior
What Remains Table-Local
These areas must stay explicit per table unless a later spec proves a shared need:
- Filters (governed separately by filament-filter-ux.md)
- Row actions, bulk actions, header actions (governed by filament-actions-ux.md)
- Query scoping
- Empty-state copy
- Domain-specific badge labels/colors/mappings