# 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 ```php 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 ```php ->placeholder('—') ``` Consistent across all tables. Do not mix blank cells, `n/a`, `unknown`, or dash variants. --- ## ID Presentation - Hidden by default - `copyable()` when operationally relevant - `limit()` and/or `tooltip()` 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 centralized `BadgeCatalog` / `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: ```php ->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: ```php ->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()` and `tooltip()` - 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](filament-filter-ux.md)) - Row actions, bulk actions, header actions (governed by [filament-actions-ux.md](filament-actions-ux.md)) - Query scoping - Empty-state copy - Domain-specific badge labels/colors/mappings