Responsive navbar

A top navbar pattern that shows how to use the navbar component.

The navbar component is relatively small on purpose: it consists of a sticky bar and a few tokens. I've found that most projects need unique navbars to fit the needs of a site. So, I chose not to create an overly complex navbar pattern. Brio is meant to be a small foundation of css that you can build on, not a complete set of tools.

But when you need to add a navbar, this pattern can be helpful to review. This pattern shows a typical use for navbars: a brand, some centered links, and actions on the end. By combining compositions with the navbar component, along with some simple show/hide utilities, you can create a responsive navbar that adapts to different screen sizes.

Why do it this way? Because it's straightforward, reuses existing styles, keeps the markup clean, and is relatively easy to maintain. Once you get used to the patterns, it's easier to keep reaching for them instead of per-class CSS or utility classes. And, you can avoid creating inconsistencies throughout a site.

The viewport handoff

Normally, a navbar spans the width of the viewport. So, when a viewport starts at a specific size, or is resized, the viewport can help decide how the navbar adapts. To make that easier to work with, you can use the existing .hide-* and .show-* utilities that are built-in to Brio.

These utilities are used to hide and show elements based on the viewport size alone. The breakpoints are hard-coded into Brio. However, you can override these via an @layer overrides directive in your project's stylesheet. See the Utilities page for more information.

The demo below reacts to the size of the viewport. So, resize the window to see how the navbar adapts. To see an inline representation, the next demo shows the same idea with container queries.

Acme


<header class="navbar">
        <div class="cluster is-between">
            <a href="#" class="nav-link is-brand"><strong>Acme</strong></a>
            <nav class="cluster hide-below-md" aria-label="Primary">
                <a href="#" class="nav-link" aria-current="page">Docs</a>
                <a href="#" class="nav-link">Components</a>
                <a href="#" class="nav-link">Pricing</a>
            </nav>
            <div class="cluster">
                <button class="button is-ghost is-sm hide-below-md" type="button">Sign in</button>
                <button class="button is-sm hide-below-md" type="button">Get started</button>
                <button
                    class="button is-ghost is-sm is-icon show-below-md"
                    type="button"
                    commandfor="app-nav-drawer"
                    command="show-modal"
                    aria-label="Open menu"
                >
                    <svg
                        width="16"
                        height="16"
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="2"
                        stroke-linecap="round"
                        aria-hidden="true"
                    >
                        <line x1="3" y1="6" x2="21" y2="6" />
                        <line x1="3" y1="12" x2="21" y2="12" />
                        <line x1="3" y1="18" x2="21" y2="18" />
                    </svg>
                </button>
            </div>
        </div>
    </header>
    <brio-drawer>
        <dialog class="drawer" id="app-nav-drawer">
            <div class="drawer-panel">
                <header>
                    <h3>Acme</h3>
                    <button
                        class="dismiss"
                        type="button"
                        commandfor="app-nav-drawer"
                        command="--dismiss"
                        aria-label="Close"
                    >
                        &times;
                    </button>
                </header>
                <section>
                    <nav class="stack is-tight" aria-label="Primary">
                        <a href="#" class="nav-link" aria-current="page">Docs</a>
                        <a href="#" class="nav-link">Components</a>
                        <a href="#" class="nav-link">Pricing</a>
                    </nav>
                    <hr class="divider" />
                    <div class="cluster">
                        <button class="button is-outlined is-sm" type="button">Sign in</button>
                        <button class="button is-sm" type="button">Get started</button>
                    </div>
                </section>
            </div>
        </dialog>
    </brio-drawer>

When the container is the unit

A navbar doesn't always span the full viewport. Sometimes it sits inside a column, an app shell, or one of the demo frames on this site. In that case, the navbar narrows when its container narrows, and the viewport utilities won't respond. That's what the container query utilities are for. Add .cq-inline to a wrapper around the navbar, then use the .cq-hide-* utilities on the elements inside. They work like the viewport versions, but they respond to the width of the wrapper.

The built-in breakpoints are on the wide side for a navbar inside a column, so the demo below uses its own @container rule with a smaller threshold. That lets the demo start expanded, and you can drag the frame to watch the navbar adapt. The same limitation is why the examples on this site use container queries: resizing a frame never changes the size of the window.

<style>
            .app-navbar-narrow .app-menu {
                display: none;
            }
            @container (max-width: 34rem) {
                .app-navbar-narrow .app-inline-nav {
                    display: none;
                }
                .app-navbar-narrow .app-menu {
                    display: inline-flex;
                }
            }
        </style>
        <div class="cq-inline app-navbar-narrow">
            <header class="navbar">
                <div class="cluster is-between">
                    <a href="#" class="nav-link is-brand"><strong>Acme</strong></a>
                    <nav class="cluster app-inline-nav" aria-label="Primary">
                        <a href="#" class="nav-link" aria-current="page">Docs</a>
                        <a href="#" class="nav-link">Components</a>
                        <a href="#" class="nav-link">Pricing</a>
                    </nav>
                    <div class="cluster">
                        <button class="button is-ghost is-sm hide-below-md" type="button">
                            Sign in
                        </button>
                        <button class="button is-sm hide-below-md" type="button">
                            Get started
                        </button>
                        <button
                            class="button is-ghost is-sm is-icon app-menu"
                            type="button"
                            commandfor="app-nav-drawer-2"
                            command="show-modal"
                            aria-label="Open menu"
                        >
                            <svg
                                width="16"
                                height="16"
                                viewBox="0 0 24 24"
                                fill="none"
                                stroke="currentColor"
                                stroke-width="2"
                                stroke-linecap="round"
                                aria-hidden="true"
                            >
                                <line x1="3" y1="6" x2="21" y2="6" />
                                <line x1="3" y1="12" x2="21" y2="12" />
                                <line x1="3" y1="18" x2="21" y2="18" />
                            </svg>
                        </button>
                    </div>
                </div>
            </header>
        </div>
        <brio-drawer>
            <dialog class="drawer" id="app-nav-drawer-2">
                <div class="drawer-panel">
                    <header>
                        <h3>Acme</h3>
                        <button
                            class="dismiss"
                            type="button"
                            commandfor="app-nav-drawer-2"
                            command="--dismiss"
                            aria-label="Close"
                        >
                            &times;
                        </button>
                    </header>
                    <section>
                        <nav class="stack is-tight" aria-label="Primary">
                            <a href="#" class="nav-link" aria-current="page">Docs</a>
                            <a href="#" class="nav-link">Components</a>
                            <a href="#" class="nav-link">Pricing</a>
                        </nav>
                    </section>
                </div>
            </dialog>
        </brio-drawer>

Pairing with a subnavbar

A .subnavbar is meant to sit directly under a .navbar, and to stay pinned below it while the page scrolls. To stick in the right place, it needs to know how tall the navbar is. That's what the --navbar-height token is for.

Brio can't set that token for you. A navbar's height comes from its padding and its content, and CSS has no way to measure the result and write it back to a custom property. So the pairing needs one small step on your side: measure the navbar once, set --navbar-height to that value, and the subnavbar sticks in the right place from then on. If you change the navbar's padding later, update the token with it. The default is 4rem, which is a reasonable estimate for a typical navbar, but it is only an estimate.

The demo below puts both bars inside a scrollable box so you can see them work together. Scroll the box. The navbar holds the top, and the subnavbar holds its position right below it, at the offset the token sets.

Scroll the box. Both bars stay put.
Section links stay pinned under the site bar, at the offset the token sets.
Change the navbar's padding and the token has to move with it.
<style>
            .app-subnav {
                --navbar-height: 3.5rem;
            }
            .app-subnav-scroll {
                block-size: 16rem;
                overflow-y: auto;
                border: var(--border-width-default) solid var(--color-border-subtle);
                border-radius: var(--radius-md);
            }
            .app-subnav-content {
                padding: var(--space-md);
            }
        </style>
        <div class="app-subnav app-subnav-scroll">
            <header class="navbar">
                <div class="cluster is-between">
                    <a href="#" class="nav-link is-brand"><strong>Acme</strong></a>
                    <nav class="cluster" aria-label="Primary">
                        <a href="#" class="nav-link" aria-current="page">Docs</a>
                        <a href="#" class="nav-link">Components</a>
                    </nav>
                </div>
            </header>
            <nav class="subnavbar" aria-label="Section">
                <div class="cluster">
                    <a href="#" class="nav-link">Overview</a>
                    <a href="#" class="nav-link" aria-current="page">API</a>
                    <a href="#" class="nav-link">Guides</a>
                </div>
            </nav>
            <div class="app-subnav-content flow">
                <div class="docs-box">Scroll the box. Both bars stay put.</div>
                <div class="docs-box">
                    Section links stay pinned under the site bar, at the offset the token sets.
                </div>
                <div class="docs-box">
                    Change the navbar's padding and the token has to move with it.
                </div>
            </div>
        </div>

When JavaScript isn't available

The show/hide utilities are plain CSS, so they behave the same whether or not JavaScript loads. The drawer is different: it opens through invoker commands. Browsers that support invoker commands handle that natively, and Brio's script covers the rest. But with scripting unavailable, the drawer won't open, and the links inside it are out of reach on narrow screens.

If you need to account for that case, the simple fallback is to let the nav wrap instead of hiding it. .cluster wraps by default, so you can drop the utility classes and the links stack into rows. The navbar gets a bit taller on small screens, but every link stays reachable.