Responsivity — how it works
Two small JavaScript engines let a component react to the space it is actually given — not the size of the window. That is the difference between a media query (asks the viewport) and a container query (asks the box). Pure Admin ships both a CSS-native path and a JS path, because some responsive moves (mounting a chart, moving a block into a menu) can't be done in CSS alone.
The two engines
| Fit — fit.js | Container Breakpoint — container-breakpoint.js | |
|---|---|---|
| Decides by | Measuring content — "does the row still fit?" | Declared width thresholds — "which named band am I in?" |
| Output | Degrades slots one at a time (hide / step-down / relocate) | One named mode reflected to [data-mode] |
| Best for | A row of things competing for one line (toolbars, the navbar) | A component that restyles wholesale at set sizes (grid → tabs → icons) |
| Event | pc:fit-relocate (on a relocated slot) | pc:breakpoint (on mode flip) |
1 · Fit — degrade a row to fit
Fit watches a horizontal container and, when the content can't fit, degrades
the lowest-priority slot first, restoring as space returns.
The navbar auto-inits; any other flex row opts in with
pureAdmin.components.fit.init(el) (or the
PureAdminNavFit hook). You mark participants with attributes:
<div class="toolbar" data-pc-fit-default-priority="20">
<span data-pc-fit="steps" data-pc-fit-priority="30"> <!-- full → smaller → gone -->
<span data-pc-fit-step="0">Pure Admin</span>
<span data-pc-fit-step="1">PA</span>
</span>
<span data-pc-fit="hide" data-pc-fit-priority="10">v2.9.0</span> <!-- drops first -->
<button data-pc-fit-ignore>Save</button> <!-- pinned, never yields -->
</div>
data-pc-fit="hide"— remove the slot when it must yield.data-pc-fit="steps"— show the largest ranked variant that fits (logo → wordmark → monogram).data-pc-fit="relocate"— move the slot somewhere else entirely (next section).data-pc-fit-priority— lower degrades first;data-pc-fit-autofolds in every child of a container;data-pc-fit-ignorepins one out.
2 · Relocation — fit decides, a sink places
A relocate slot doesn't just hide — it moves out of the
row into a named destination and comes back on widen. Crucially, the
Fit engine only decides a slot must yield; where it
goes is a pluggable sink named by
data-pc-fit-target. Two sinks ship built-in:
target="floating-menu"— folds into a "•••" flyout panel. Self-contained; needs no sidebar.target="sidebar"— rebuilds the slot as a sidebar list item.- your own —
pureAdmin.components.fit.registerSink("name", { out, in }).
Live — drag to narrow the bar.
The badge cluster is one relocate slot targeting
floating-menu; when the bar runs out of room it folds into the
"•••" panel, and returns when you widen. "New" is pinned with
data-pc-fit-ignore.
<span data-pc-fit="relocate" data-pc-fit-target="floating-menu"> <span class="pa-badge">Users 123</span> … </span>
3 · The event & hands-off mode (Svelte / Phoenix)
Before it moves anything, Fit fires a cancelable pc:fit-relocate
event on the slot (detail = { action, target, container }).
A plain HTML page lets the built-in sink do the DOM move. A reactive framework
does the opposite: it owns placement itself.
Set data-pc-fit-managed (or call preventDefault())
and Fit performs no DOM surgery — it only hides the slot
in-row and tells you it flipped. The framework then re-renders the block in
its new home from state. That solves the staleness trap: a moved DOM
node keeps its old Users: 123; a re-render is bound to the live
assigns, so the relocated copy is always fresh.
// fit fires a DOM CustomEvent on the slot. In managed mode NO node is moved —
// you attach the listener the way your framework attaches DOM listeners.
// In a LiveView hook:
mounted() {
this.el.addEventListener("pc:fit-relocate", (e) => {
// e.detail = { action: "out" | "in", target, container }
this.pushEvent("relocated", { out: e.detail.action === "out" })
})
}
// The server assigns the mode and renders ONLY the branch for the current
// placement — never a stale off-screen copy:
<.flyout :if={@relocated}><.stats users={@users} rooms={@rooms} subs={@subs} /></.flyout>
<.stats :if={!@relocated} users={@users} rooms={@rooms} subs={@subs} />
pc:fit-relocate with pushEvent, the server assigns
the mode, and only the branch for the current placement is rendered — never a
stale off-screen copy.
4 · Container Breakpoint — named modes at set widths
When a component restyles wholesale at set sizes — not a row shedding
pieces, but a card that becomes a grid, then tabs, then icons — declare width
thresholds and let the engine name the band. It's the JS counterpart to a CSS
@container query, for the cases CSS can't reach (mount a chart,
push to the server).
<div phx-hook="PureAdminContainerBreakpoint"
data-pc-breakpoints='{"icons":0,"tabs":34,"grid":64}'
data-pc-breakpoint-initial="tabs">
<div data-pc-show="grid">…rich grid…</div> <!-- .d-none unless mode = grid -->
<div data-pc-show="tabs icons">…compact…</div>
</div>
- Thresholds are rem by default (root font is 10px, so
34= 340px,64= 640px); adddata-pc-breakpoint-unit="px"for pixels. - The engine reflects the band to
[data-mode](CSS can key off it) and toggles.d-noneondata-pc-showpieces. - It fires
pc:breakpointonly on a flip — the hook for "mount on demand" (build the chart ingrid, destroy it otherwise). - A small hysteresis dead-band stops flip-flopping right at a threshold.
grid mode and destroys it below, logging every flip. A standalone
engine demo lives at
Container Breakpoint.
Which one do I reach for?
One component that reshapes at set widths? Container Breakpoint — it names the band and you style/mount per mode.
Purely cosmetic swap with no mount/DOM move? A plain CSS
@container query — no JS at all. Reach for an engine
only when a move needs JavaScript: relocating a node, or mounting/destroying
on demand.
Ready to see Fit degrade real cards and toolbars across several strategies? Head to Fit to Size for the worked, slider-driven examples.