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>

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

  1. A trigger button with commandfor and command="show-modal" opens the drawer, no JavaScript required.
  2. The <dialog class="offcanvas"> holds the panel content. showModal() (via the command) puts it in the top layer with a backdrop and traps focus.
  3. closedby="any" lets the reader dismiss the drawer with Escape or a backdrop click; a command="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 (or aria-label if 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.