A settings page is a long form with a short navigation beside it. The navigation stays put while the sections scroll; the form decides for itself when its headings stand beside their fields and when they stack.
Settings
<div class="page bg-surface-base fg-text-body text-sm"> <a class="skip" href="#settings-main">Skip to content</a> <header class="navbar"> <a class="navbar-brand" href="#"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" /> </svg>Atlas</a> <nav class="navbar-nav" aria-label="Main"> <a class="navbar-link" href="#" aria-current="page">Product</a> <a class="navbar-link" href="#">Pricing</a> <a class="navbar-link" href="#">Docs</a> <a class="navbar-link" href="#">Changelog</a> </nav> <div class="navbar-end"> <span class="avatar avatar-sm"> <img class="avatar-image" src="/demo/portrait-1.svg" alt="Ada Lovelace"> </span> <button type="button" class="btn btn-quiet btn-square btn-sm navbar-toggle" popovertarget="settings-menu" aria-label="Open the menu"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M4 8h16M4 16h16" /> </svg> </button> <div class="menu menu-end" id="settings-menu" popover> <a class="menu-item" href="#">Product</a> <a class="menu-item" href="#">Pricing</a> <a class="menu-item" href="#">Docs</a> <a class="menu-item" href="#">Changelog</a> </div> </div> </header> <main class="grow(1) w-full max-w(1040) mx-auto px-lg py-xl container-inline" id="settings-main" tabindex="-1"> <header class="flex flex-col gap-3xs mb-lg"> <h1 class="heading heading-xl">Settings</h1> <p class="m(0) fg-text-muted">Manage the Atlas project.</p> </header> <nav class="tabs mb-lg hidden@container(--4xl)" aria-label="Settings"> <a class="tabs-item" href="#" aria-current="page">General</a> <a class="tabs-item" href="#">Members</a> <a class="tabs-item" href="#">Billing</a> <a class="tabs-item" href="#">Domains</a> <a class="tabs-item" href="#">API keys</a> <a class="tabs-item" href="#">Notifications</a> </nav> <div class="flex gap-xl items-start"> <nav class="nav w(200) shrink(0) sticky top(72) hidden flex@container(--4xl)" aria-label="Settings"> <a class="nav-item" href="#" aria-current="page">General</a> <a class="nav-item" href="#">Members</a> <a class="nav-item" href="#">Billing</a> <a class="nav-item" href="#">Domains</a> <a class="nav-item" href="#">API keys</a> <a class="nav-item" href="#">Notifications</a> </nav> <div class="grow(1) basis(0) min-w(0)"> <form class="form"> <section class="form-section" aria-labelledby="set-project"> <div class="form-heading"> <h2 class="form-title" id="set-project">Project</h2> <p class="form-description">The name and address people see.</p> </div> <div class="form-fields"> <label class="field">Name <input class="input" name="name" value="Atlas"> </label> <div class="field"> <label for="set-domain">Address</label> <div class="input-group"> <span class="input-group-addon">https://</span> <input class="input-group-field" id="set-domain" name="domain" value="atlas"> <span class="input-group-addon">.atlas.dev</span> </div> <span class="field-hint">Changing it breaks existing links after 30 days.</span> </div> </div> </section> <section class="form-section" aria-labelledby="set-build"> <div class="form-heading"> <h2 class="form-title" id="set-build">Build</h2> <p class="form-description">How a push becomes a deployment.</p> </div> <div class="form-fields"> <div class="form-row"> <label class="field">Framework <select class="select" name="framework"> <option class="select-option">Astro</option> <option class="select-option">Next.js</option> <option class="select-option">SvelteKit</option> </select> </label> <label class="field">Node version <select class="select" name="node"> <option class="select-option">24</option> <option class="select-option">22</option> </select> </label> </div> <label class="check"> <input type="checkbox" class="checkbox" name="previews" checked>Build a preview for every branch</label> <label class="check"> <input type="checkbox" class="checkbox" name="cancel">Cancel a build when a newer push arrives</label> </div> </section> <div class="form-actions"> <button type="button" class="btn">Discard</button> <button class="btn btn-primary">Save changes</button> </div> </form> </div> </div> </main></div>At Tablet and Phone the side navigation becomes a row of tabs over the form.
#How it is built
- A page with one measure. The main region is
max-w(1040) mx-autoand a size container,container-inline. - Two columns from 56rem. A fixed
w(200)navigation and a fluid form,grow(1) basis(0) min-w(0), in aflexrow. - Two navigations, one on show. Below 56rem of main region the tabs are shown and the nav is
hidden; from 56rem it is the other way round, withhidden@container(--4xl)on one andhidden flex@container(--4xl)on the other. The one that is hidden isdisplay: none, so assistive technology meets only one. - The form looks after itself.
form-sectionputs each heading beside its fields from 40rem of form and above them below that;form-rowandform-actionsdo the same at their own widths. The page's layout and the form's never need to agree on a breakpoint.
The side navigation is sticky top(72): it stays 16px under the 56px navbar while the sections scroll. That is all "stays put" needs, because the document is what scrolls.
#One page or many
Give each group its own page when saving one should not save another: a "Save changes" button at the foot of a long page is easy to miss, and a reader cannot tell which sections it covers. On one page, save each section by itself or save on change, and say which.
#Accessibility
- Both navigations carry the same label; only one is in the accessibility tree at a time.
- Mark the current group with
aria-current="page". - Each section of the form is a
<section>named by its title.
#Related
- FormThe rhythm of a form: rows, sections and actions that follow their column.
- NavbarThe bar at the top of a page, which takes its material once content scrolls under it.
- Skip linkThe first link of a page, shown while it has focus, that jumps past the navigation.
- NavA vertical list of destinations for a sidebar.
- TabsWords on one hairline, the current one underlined in ink.
- InputText fields cut into the surface, with labels, hints, errors and groups.
- SelectA native select whose option list is drawn in the same material.
- CheckboxA small native well that fills with ink when checked.
- MenuA list of actions in a popover, placed against its button.