vitisstudio/filament-header-schema
Build Filament page headers with a schema-based API. Compose titles, actions, breadcrumbs, and custom header content consistently across resources and custom pages, cutting boilerplate and keeping headers clean, reusable, and easy to maintain.
Install and pin. composer require vitisstudio/filament-header-schema, then immediately open the published composer.json constraints and confirm the Filament major/minor range is declared, not merely observed. With zero dependents and a single maintainer, pin an exact version ("vitisstudio/filament-header-schema": "1.1.0") rather than a caret range. There is no herd to absorb a breaking Filament minor ahead of you.
Read the source before the README. The package is small — a service provider, a page concern/trait exposing headerSchema(), the HeaderSection primitive, and a CSS file. Thirty minutes in src/ and resources/ will tell you more than the docs about which Filament components are actually supported, whether CSS ships precompiled or expects your Tailwind build to scan its Blade, and how the container wrapper is emitted. Do this before writing any schema.
Pick one page, not the codebase. The right first use case is a single record-detail page whose header you already hand-built in Blade: an avatar, a name, a badge row, a customer, a price and a date. Convert that page. Keep the Blade partial in place behind a flag or in git history until the new header has shipped for a sprint. Do not standardise house conventions until two or three conversions are done.
The first change is one method. On the page class (typically a ViewRecord page), add headerSchema() returning an array of Filament schema components, and leave getHeaderActions() exactly as it is. Confirm the actions row still renders — the package positions it; you do not.
Verify at the right width. Open the browser, open the sidebar, and screenshot the header at container widths either side of 64rem. The v1.1.0 fix is specifically about the case where a 1100px window is 800px of content. A full-window screenshot proves nothing.
Treat the header as three slots, not a blob. HeaderSection exposes leading / main / trailing, which maps cleanly onto identity / facts / commercial context. Put the avatar in leading, the name and badge row in main, the price and dates in trailing. Resist filling main with everything — the trailing slot is what keeps the header scannable when it stacks.
Extract shared headers into their own class. The pitch that matters at scale is reuse across panels. A static CustomerHeaderSchema::make(): array returning the component array, called from headerSchema() on every page that shows a customer, gives you one place to change a badge or reorder a fact. This is also what makes header changes reviewable as schema diffs rather than template diffs.
Let Filament's action API own the buttons. getHeaderActions() continues to route through Filament's action system, including authorization, modals and confirmation. Don't try to render buttons as schema components — you lose all of that. The package's job is the content above the fold; actions stay where they are.
Drive badges from model state, not from the schema. Badge::make($record->status)->color($record->status_color) style bindings keep the schema declarative and let the model own presentation logic. The same applies to any computed price or date formatting.
Gate the commercial facts. If trailing holds a price or margin, wrap its visibility in the same policy check you'd use anywhere else (->visible(auth()->user()->can('viewFinancials', $record))). The header is a rendered region like any other; it is not exempt from authorization just because it is chrome.
Eager-load before the schema renders. The header renders on every Livewire re-render of the page. Anything the schema touches — customer, status, latestOrder — must be in the page query's with(). A header that quietly N+1s on a poll or a table sort is the most likely performance regression you will introduce.
Roll out one page per pull request, with screenshots in the description. Because the header responds to container width, "works on my machine" is not sign-off. Attach before/after at sidebar-open and sidebar-closed. That artifact is also what your reviewer will want when a Filament minor shifts the DOM.
Test what you can cheaply. Livewire tests asserting assertSee($record->name) and the presence of key facts catch regressions in the schema. They will not catch layout regressions — there is no evidence of layout test coverage upstream, so budget manual visual QA per upgrade.
Two layout models live in one header. The actions row answers the header's own container width (@container queries against fi-hs-header). HeaderSection's own ->from() breakpoints are still media queries against the window. They will disagree in exactly the situations you care about — narrow column, open sidebar, split pane. Never assume the actions and the slots break together. The maintainer has deliberately deferred this; treat it as a known bug you are accepting.
v1.1.0 moved pixels. Headers between 40rem and 64rem of container width now stack where they used to sit side by side. "No API changes" is true and irrelevant to your visual QA — re-open sign-off on every page you converted before the upgrade.
There is an extra wrapper element. <div class="fi-hs-header-ctn"> now sits inside the header. Any sibling-position CSS, any :first-child/:nth-child test, any Blade assertion about the header's place among its siblings needs checking. Nothing in Filament selects .fi-header as a direct child, so the risk is yours, not theirs.
The package couples to .fi-* markup. v1.1.0 resets Filament's breadcrumb offset and asserts on header spacing — that is a dependency on undocumented internals. When you bump Filament, open the package's CSS diff first and then check the header visually. This is the coupling most likely to bite you, and it bites silently.
Browser floor. Container queries mean roughly Chrome 105 / Safari 16 / Firefox 110. Fine for internal admin tooling. Name it explicitly if any kiosk, embedded webview or locked-down fleet touches the panel — a broken header there is not a graceful degradation.
Tailwind build questions decide your upgrade path. Does the package ship compiled CSS, or does your app's Tailwind build need to scan its Blade files? If it's the latter, a v3→v4 migration in your app can silently stop generating the package's classes. Check content globs in tailwind.config.js and confirm the package's views are covered.
Debugging a wrong layout. Inspect the .fi-hs-header-ctn element in devtools and read its computed inline size — that, not the viewport, is what the action row is responding to. Then check whether anything between it and the header has container-type set, which would rebind the query. The container is named fi-hs-header precisely to avoid that; a new wrapper of yours can still introduce it.
Don't do work in the schema. Schema closures run on render. Formatting a price is fine; querying an aggregate is not. Compute once in mount() or on the model and bind the result.
Keep the exit cheap. Uninstalling is a per-page rewrite back to Blade, not a config flip. Keeping the old partials in history and converting one page at a time is what makes the decision reversible — protect that by not letting the schema leak into anything outside the header region.
Watch upstream against Filament releases. The cheapest health signal is commit history versus Filament's last two minors. If it falls behind, vendor the package into your repo; at this size that is an afternoon, and it converts a supply-chain risk into code you own with an upstream.
How can I help you explore Laravel packages today?