Appearance
A responsive toolbar with CSS container queries, no JavaScript
A responsive toolbar built with CSS container queries sizes itself from the width the bar actually has, not the width of the window, and hides controls in a fixed priority order while a More menu picks them up. That is how the FilamentCraft editor topbar works since v1.40.4. Before it, the bar used viewport breakpoints, and between 1280px and 1440px the view controls slid under the language picker. This post covers why viewport media queries failed for a toolbar inside a Filament panel, how the tiers are laid out, and the two rules of container queries that shaped the markup.
What broke
The editor topbar holds a lot: the page and language pickers, undo and redo, device switcher, inspector and focus-mode toggles, auto-save state, search, settings, the live-page link, and the save and publish buttons. On a wide monitor that fits. The v1.40.4 CHANGELOG entry describes what happened below that: between 1280px and 1440px "the view controls ran under the language picker, and below 768px button labels spilled over the next button."
The cause was not the number of controls. It was what the breakpoints measured. The editor runs inside a Filament panel, and the space the topbar gets depends on things the viewport does not know about: the panel sidebar, a docked slide-over, browser zoom. At a 1366px window the bar might have 1366px or it might have a lot less, and a @media (max-width: 1280px) rule has no way to tell those apart.

The topbar is its own size container
The fix starts with one declaration. The bar becomes a named size container:
css
.fc-topbar,
.fc-editor__topbar .fc-topbar {
container: fc-topbar / inline-size;
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}The container shorthand sets container-name and container-type in one line (MDN). inline-size means queries can ask about the bar's width and nothing else, which is also the cheaper kind of containment: the browser does not need to know the bar's height in advance, so it can still grow to fit its content vertically.
Naming the container is cheap insurance. An unnamed @container (max-width: 900px) resolves against the nearest ancestor that is a size container, whichever that happens to be, so a wrapper someone adds later between the bar and its buttons would quietly change what every tier measures. @container fc-topbar (...) always means this bar.
Five tiers, in priority order
With the container in place, the tiers are ordinary CSS. Each one hides a little more and reveals the matching entries in the More menu. Here is the second tier, trimmed:
css
@container fc-topbar (max-width: 1180px) {
.fc-topbar__more {
display: inline-flex;
}
.fc-topbar__more [data-fc-more="history"],
.fc-topbar__more [data-fc-more="view"] {
display: list-item;
list-style: none;
}
.fc-topbar__cluster--ghost,
.fc-topbar__discard,
.fc-topbar__status,
.fc-topbar__cluster-btn--inspector,
.fc-topbar__cluster-btn--immersive {
display: none;
}
}The full order, from what leaves first to what leaves last:
- At 1320px, labels go: the discard label and the auto-save status label.
- At 1180px, undo and redo, auto-save, the hover-controls and focus-mode toggles leave the bar, and the More trigger appears with the history and view entries.
- At 900px, search, settings and the live-page link leave, save and publish drop their labels, and the page entries appear in More.
- At 640px, spacing tightens and the page name truncates harder.
- At 460px, the device switcher leaves the bar and shows up as a row of device buttons at the top of More.
Save and publish never leave. They lose their labels at 900px and keep their icons, because a toolbar that hides its primary action behind a menu has hidden the one thing people came to press.
One menu, rendered once
The More menu is plain Blade rendered on every request, at every width. Each item carries a data-fc-more attribute naming its group (history, view, page, devices), and the whole menu is hidden by default:
css
.fc-topbar__more,
.fc-topbar__more [data-fc-more] {
display: none;
}The tiers switch groups on as their bar controls switch off. Because the menu is already in the DOM, there is nothing to measure and nothing to move. There is no ResizeObserver, no IntersectionObserver, and no JavaScript that clones buttons into a dropdown after layout. Alpine only handles opening and closing the menu (x-data="{ open: false }", closing on outside click and Escape).
The trade-off is fixed breakpoints. A JavaScript overflow menu, like the one in CSS-Tricks' container-adapting tabs, measures each item and moves exactly as many as do not fit. Ours moves whole groups at chosen widths. For a bar whose contents we control, predictability won: a control is either in the bar or in More at a given width, never in a state that depends on the pixel width of a translated label. It also works on the first paint, before any script runs.
Names that shrink instead of pushing
Priority tiers only work if nothing forces the bar wider than its container. Two things did: long page names and long language names. Flex items default to min-width: auto, which means "never narrower than my content", so one long page title pushed the whole right-hand zone off the edge.
The fix is min-width: 0 down the chain and an ellipsis at the end:
css
.fc-topbar__group--page {
min-width: 0;
}
.fc-topbar__group--page .fc-topbar__dropdown-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 14rem;
}The tiers then lower max-width (9rem at 900px, 6.5rem at 640px) and remove the page and language labels entirely at 460px.
The container cannot style itself
One rule of container queries decides how you write the selectors. A container query applies styles to descendants of the container, never to the container element itself. MDN's example comment on the same page says it directly: "styles applied to descendants of this size container".
So the tiers do their work on the bar's children: zones, groups, buttons and labels. If you want the bar's own padding or gap to change with its width, that change has to come from a query against some ancestor container, or you move the flex layout one level down into a child that the query can reach. It is easy to forget, because the rule looks correct and the browser does not warn you that it never matches. Our own 640px tier still carries a rule that sets the bar's gap and padding, and it does nothing for exactly this reason; the tighter spacing you see at that width comes from the right-hand zone's gap, which is a descendant.
The phone drawer, and why off-screen is not hidden
The same release fixed the section drawer on phones. Below 768px the sidebar is an off-canvas drawer slid out with transform, and a transformed element is still focusable: keyboard users could tab into a drawer they could not see. The fix is visibility: hidden on the closed drawer, with the visibility change delayed until the slide-out animation ends:
css
.fc-editor__rail {
visibility: hidden;
transition: transform 0.18s ease, visibility 0s linear 0.18s;
}
.fc-editor--rail-open .fc-editor__rail {
visibility: visible;
transition: transform 0.18s ease, visibility 0s linear 0s;
}Opening flips visibility immediately. Closing waits 0.18s, so the drawer stays visible while it slides away and becomes unfocusable once it is gone. The closed drawer also stopped casting its shadow over the canvas edge.
Try it
The easiest way to see the tiers is to open the editor and drag the window narrower, or open a Filament slide-over next to it. The editor guide covers each topbar control, and editor internals explains how the canvas iframe and the panel talk to each other. The live demo runs the current release.
