Navigation

The floating navigation that defines Joy's look: a pill-shaped topbar and a pill-shaped dock. Both are optional — a site can use only the content classes (cards, buttons, forms) with none of this. The dock in particular is the one piece of Joy other design systems don't have anything quite like, so its tab goes deep.

joy.js looks for these elements automatically on DOMContentLoaded, by the ids documented below. If one is missing from the page, that piece of setup just does nothing — you don't need to remove any code, only skip the markup. Everything on this page — position, color — is opt-in: a plain .joy-topbar or .joy-dock with no extra class renders exactly like it always has.

Markup

<nav class="joy-topbar joy-glass joy-rounded-pill">
    <a href="/"><img src="/logo.png" alt="Brand" style="width:26px;height:26px;"></a>
    <button type="button" class="joy-btn joy-btn-ghost joy-btn-icon" id="joy-theme-toggle"><i class="ph ph-moon"></i></button>
    <div id="joy-topbar-auth-slot">
        <a href="/login" class="joy-btn joy-btn-ghost"><i class="ph ph-sign-in"></i><span>Log in</span></a>
        <a href="/register" class="joy-btn joy-btn-solid"><i class="ph ph-user-plus"></i><span>Sign up</span></a>
    </div>
</nav>

Shown as code, not a live preview — .joy-topbar is position: fixed by design (it's meant to float over the whole page, once), so a second real one on this page would fight with the docs site's own topbar above for the same #joy-theme-toggle id. The variants further down use throwaway ids so they're safe to render for real.

PieceRole
#joy-theme-togglewires up JoyTheme.toggle() on click, if present
#joy-topbar-auth-slotswapped by JoyAuth.renderLoggedIn(user) after a login, no reload needed

User menu (dropdown)

<div class="joy-user-menu" id="joy-user-menu">
    <button type="button" class="joy-btn joy-btn-ghost" id="joy-user-trigger" aria-haspopup="true">
        <img src="/avatar.png" alt="" class="joy-user-avatar">
        <span>Jane Doe</span>
        <i class="ph ph-caret-down"></i>
    </button>
    <div class="joy-user-dropdown" id="joy-user-dropdown">
        <a href="/profile" class="joy-user-dropdown-item"><i class="ph ph-user"></i> My profile</a>
        <a href="/logout" class="joy-user-dropdown-item"><i class="ph ph-sign-out"></i> Log out</a>
    </div>
</div>

All three ids (#joy-user-menu, #joy-user-trigger, #joy-user-dropdown) are required together for the open/close toggle to work.

Position

The default topbar sits top-right. Two modifier classes move it — nothing else about it changes:

ClassEffect
none (default)top-right corner
.is-lefttop-left corner
.is-centertop center
<nav class="joy-topbar joy-glass joy-rounded-pill is-left">
    <span style="padding:0 8px;font-weight:700;">Brand</span>
    <button type="button" class="joy-btn joy-btn-ghost joy-btn-icon"><i class="ph ph-moon"></i></button>
</nav>

Color

Three modifiers, layered on top of (or instead of) .joy-glass. Combine with a position class freely:

ClassEffect
.is-solidopaque --joy-surface background, no blur — for a page where the glass effect fights with what's behind it
.is-darkforces a dark shell regardless of the site's active theme — locally overrides --joy-text/--joy-text-muted, so every button and icon inside adjusts on its own
.is-accentsolid brand-color shell — also flips .joy-btn-solid inside it so a "Sign up" button stays visible against its own accent background
<nav class="joy-topbar joy-glass joy-rounded-pill is-dark" style="position:static;transform:none;top:auto;right:auto;">
    <span style="padding:0 8px;font-weight:700;">Dark</span>
    <button type="button" class="joy-btn joy-btn-ghost joy-btn-icon"><i class="ph ph-moon"></i></button>
    <a href="#" class="joy-btn joy-btn-solid">Sign up</a>
</nav>
<nav class="joy-topbar joy-glass joy-rounded-pill is-accent" style="position:static;transform:none;top:auto;right:auto;">
    <span style="padding:0 8px;font-weight:700;">Accent</span>
    <button type="button" class="joy-btn joy-btn-ghost joy-btn-icon"><i class="ph ph-moon"></i></button>
    <a href="#" class="joy-btn joy-btn-solid">Sign up</a>
</nav>

That example pins position: static inline just so both variants sit side by side on this page instead of both trying to occupy the real top-right corner — on your own site, drop that override and let .joy-topbar's real fixed position do its job.

Tutorial: a themed topbar for a landing page

Say you're shipping a one-off marketing page and want the topbar anchored top-left, in your brand color, without writing a single line of custom CSS:

<nav class="joy-topbar joy-glass joy-rounded-pill is-left is-accent">
    <a href="/"><img src="/logo-white.png" alt="Brand" style="width:26px;height:26px;"></a>
    <a href="/pricing" class="joy-btn joy-btn-ghost">Pricing</a>
    <a href="/signup" class="joy-btn joy-btn-solid">Get started</a>
</nav>

That's the entire change: two extra classes on an element you already had. No override stylesheet, no specificity fights — .is-left and .is-accent are real modifier classes shipped in joy.css, not a pattern you're expected to recreate yourself.

Markup

Shown as code for the same reason as the topbar above — it's position: fixed, meant to appear once. The variants further down are safe to render live since this page has no real dock competing with them.

<nav class="joy-dock joy-glass">
    <button type="button" class="joy-dock-arrow joy-dock-prev" aria-label="Previous"><i class="ph ph-caret-left"></i></button>
    <a href="/" class="joy-dock-item is-active"><i class="ph ph-house"></i><span class="joy-dock-label">Home</span></a>
    <a href="/products" class="joy-dock-item"><i class="ph ph-squares-four"></i><span class="joy-dock-label">Products</span></a>
    <a href="/about" class="joy-dock-item"><i class="ph ph-info"></i><span class="joy-dock-label">About</span></a>
    <button type="button" class="joy-dock-arrow joy-dock-next" aria-label="Next"><i class="ph ph-caret-right"></i></button>
</nav>

On desktop, each item expands on hover to show its label; on mobile (≤600px) it collapses into a ‹ Icon Label › selector with arrows, and swiping switches tabs.

DetailBehavior
Active itemthe .is-active class on one .joy-dock-item
Clicking a linknavigates normally — the dock never intercepts a click in plain anchor mode
Tapping the active tab (mobile)opens a "jump to" popup, built automatically, listing every tab
Dragging on mobilefollows your finger in real time, with an axis lock so it doesn't fight vertical scrolling

The mobile dock uses touch-action and dedicated touch listeners — if your page already has its own swipe/carousel system, test the two together before shipping.

Position

Default is bottom-center. Three modifiers move it, and they combine — .is-top.is-right is a real corner, not just two separate effects fighting each other:

ClassEffect
none (default)bottom center
.is-toptop instead of bottom
.is-leftleft edge instead of centered
.is-rightright edge instead of centered
.is-top.is-righttop-right corner (any top/left/right pair combines the same way)

Live, in its own frame (a real dock this small only makes sense as a demo — on your page it would anchor to the actual viewport corner):

<nav class="joy-dock joy-glass is-top is-right">
    <a href="#" class="joy-dock-item is-active"><i class="ph ph-house"></i><span class="joy-dock-label">Home</span></a>
    <a href="#" class="joy-dock-item"><i class="ph ph-squares-four"></i><span class="joy-dock-label">Products</span></a>
</nav>

Color

ClassEffect
.is-solidopaque --joy-surface background, no blur
.is-darkforces a dark shell regardless of the site's theme — item backgrounds, hover, and the active-item dot all adjust to stay legible
.is-accentsolid brand-color shell — the active/hovered item flips to a light pill so it still reads clearly against the accent background
<nav class="joy-dock joy-glass is-dark" style="position:static;transform:none;bottom:auto;">
    <a href="#" class="joy-dock-item is-active"><i class="ph ph-house"></i><span class="joy-dock-label">Home</span></a>
    <a href="#" class="joy-dock-item"><i class="ph ph-squares-four"></i><span class="joy-dock-label">Products</span></a>
</nav>
<nav class="joy-dock joy-glass is-accent" style="position:static;transform:none;bottom:auto;">
    <a href="#" class="joy-dock-item is-active"><i class="ph ph-house"></i><span class="joy-dock-label">Home</span></a>
    <a href="#" class="joy-dock-item"><i class="ph ph-squares-four"></i><span class="joy-dock-label">Products</span></a>
</nav>

Same as the topbar example above: position: static is pinned inline only so both variants render side by side on this page. Drop it on your own site and the dock anchors to the real corner you picked with the position classes.

Tutorial: a dark, top-right dock

The dock is Joy's signature piece — most teams that reach for it want to make it unmistakably theirs. Here's a fully repositioned, recolored dock, still just markup and classes:

<nav class="joy-dock joy-glass is-top is-right is-dark">
    <a href="/" class="joy-dock-item is-active"><i class="ph ph-house"></i><span class="joy-dock-label">Home</span></a>
    <a href="/projects" class="joy-dock-item"><i class="ph ph-folder"></i><span class="joy-dock-label">Projects</span></a>
    <a href="/settings" class="joy-dock-item"><i class="ph ph-gear"></i><span class="joy-dock-label">Settings</span></a>
</nav>

Want it to match your brand color instead of a neutral dark? Swap .is-dark for .is-accent — same markup, same three items, completely different read. Nothing here required forking joy.css or writing an override stylesheet; the modifier classes are the whole point of shipping them natively instead of leaving this to copy-pasted custom CSS.