Fylgja Offcanvas
An offcanvas is a panel that slides in from a screen edge over a dimmed page, used
for navigation, filters, or a side panel. It is the same native <dialog> as the
Modal, opened with an invoker command, just pinned to an
edge instead of centered. Fylgja’s base dialog styles do the heavy lifting, so the
only addition is the small .offcanvas rule.
Preview
<button type="button" commandfor="menu" command="show-modal">Open menu</button>
<dialog id="menu" class="offcanvas open:flex flex-col clip" closedby="any" aria-labelledby="menu-title" style="--p: 0">
<div class="flex align gap flow-none" style="--py: var(--size-6); --px: var(--size-6)">
<h2 id="menu-title">Menu</h2>
<button type="button" commandfor="menu" command="close" aria-label="Close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<nav class="flex-auto scroll-y scroll-mask" style="--py: var(--size-2) var(--size-6); --px: var(--size-2)">
<ul style="--btn-stroke: transparent">
<li><a href="/" class="btn w-full align">Home</a></li>
<li><a href="/library/base/" class="btn w-full align">Library</a></li>
<li><a href="/ui/" class="btn w-full align">UI Components</a></li>
</ul>
</nav>
</dialog><div x-data="{ open: false }">
<button type="button" @click="open = true">Open menu</button>
<dialog x-show="open" x-htmldialog.closeby.any="open = false" class="offcanvas open:flex flex-col clip" aria-labelledby="menu-title" style="--p: 0">
<div class="flex align gap flow-none" style="--py: var(--size-6); --px: var(--size-6)">
<h2 id="menu-title">Menu</h2>
<button type="button" @click="open = false" aria-label="Close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<nav class="flex-auto scroll-y scroll-mask" style="--py: var(--size-2) var(--size-6); --px: var(--size-2)">
<ul style="--btn-stroke: transparent">
<li><a href="/" class="btn w-full align">Home</a></li>
<li><a href="/library/base/" class="btn w-full align">Library</a></li>
<li><a href="/ui/" class="btn w-full align">UI Components</a></li>
</ul>
</nav>
</dialog>
</div>The .offcanvas rule
The base dialog styles already provide the backdrop, the slide in and out (via
the --tx / --ty translate with @starting-style and a reduced-motion-aware
transition), and the background scroll-lock. Never redeclare those. All a drawer
adds is this rule, which retargets those base variables and pins the panel to the
inline-end (right) edge, full height, up to 25rem wide:
:where(dialog).offcanvas {
--ty: 0;
--tx: 100%;
--screen-y: 0;
--mx: auto 0;
block-size: 100%;
inline-size: min(100%, 25rem);
border-start-end-radius: 0;
border-end-end-radius: 0;
}
Ship this rule with the component. Fylgja’s theme.css already includes it, but
theme.css is one of the files Fylgja lets you copy into a project and customize,
so you cannot assume every project loads it. Only the Fylgja base styles are safe
to rely on, so include the rule rather than depend on theme.css being there.
Usage
- A trigger button with
commandforandcommand="show-modal"opens the drawer, no JavaScript required. - The
<dialog class="offcanvas">holds the panel content.showModal()(via the command) puts it in the top layer with a backdrop and traps focus. closedby="any"lets the reader dismiss the drawer with Escape or a backdrop click; acommand="close"button gives an explicit control too.
The Alpine version
The Alpine tab above opens the same <dialog> from Alpine state with
x-htmldialog. That directive comes from the
AlpineJS Dialog plugin, whose docs cover
installing it and the modifiers it takes.
Anatomy
| Part | Responsibility |
|---|---|
Trigger <button command> |
Opens the drawer via command="show-modal" and commandfor. |
<dialog class="offcanvas"> |
The panel. Base styles animate and back it; .offcanvas pins it. |
command="close" button |
Explicit dismiss, alongside Escape and the backdrop. |
Accessibility
- Label the panel with
aria-labelledby(oraria-labelif there is no heading). - Opened as a modal, the drawer traps focus and makes the page inert; focus returns to the trigger on close, natively.
- With
closedby="any"the reader can leave via Escape, the backdrop, or the close button. Keep a visible close control as well.
FAQ
Do I need to write custom CSS for the drawer?
Almost none. Fylgja's base dialog styles already provide the backdrop, the slide animation, and the background scroll-lock. All a drawer adds is the small .offcanvas rule shown below, which only retargets a few base variables.
How is an offcanvas different from a modal?
They are the same native <dialog> primitive. A modal sits centered on the page, an offcanvas is pinned to an edge and fills the height. Use an offcanvas for navigation, filters, or a side panel.
Can I slide it in from the left instead of the right?
Yes, by overriding the variables the .offcanvas rule sets (--tx, --mx) and flattening the start-side corners instead. Check @fylgja/base/theme.css for the exact values rather than guessing.
What does the Alpine version need to work?
The x-htmldialog directive comes from the @fylgja/alpinejs-dialog plugin, which makes x-show call showModal instead of toggling display, so you keep the top layer, the backdrop and the focus trap. The example markup leaves the setup out on purpose; install and registration live in the AlpineJS Dialog docs.