Novus Technologies Novus Technologies design kit v0.9.0
Menu

Navigation

Side navigation

Two sidebars from one mechanism: grouped (NovaCard) with the appbar hamburger, flat (NovaPulse) with a chevron on its own edge. Both collapse to an icon rail on desktop and open as a drawer on phones, without JavaScript.

.adminnav · .navgroup · .navlink · .navtoggle__input · .navscrim · .adminnav--flat · .navtoggle--edge

operations · admin

Screen content. The menu button collapses the navigation at desktop width; on a phone it opens the navigation as a drawer.

Grouped sidebar (NovaCard): destinations under areas, so the appbar hamburger collapses it
<div class="adminwrap" style="border:1px solid var(--border);border-radius:var(--radius-md)">
  <input type="checkbox" id="navtoggle-demo" class="navtoggle__input" aria-label="Show or hide the menu">
  <header class="appbar">
    <div class="appbar__inner appheader">
      <div class="appheader__left">
        <novus-app-switcher current="novapay" launcher="https://workspace.novustech.dev" catalog="../demos/app-switcher/sample-catalog.json"></novus-app-switcher>
        <label for="navtoggle-demo" class="btn btn--ghost btn--sm toggle44 navtoggle" title="Menu"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></label>
        <span class="theme-novapay vlogo"><svg class="vmark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="6" width="17" height="12" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><rect x="6" y="12" width="3.6" height="3" rx="0.6"/></svg><span class="wm"><span class="nm">nova</span><span class="sf">pay</span></span></span>
        <span class="muted appsuffix">operations · admin</span>
      </div>
      <div class="appheader__right"><span class="appheader__endorse"><span class="brandmark" role="img" aria-label="Novus Technologies"></span></span></div>
    </div>
  </header>
  <label for="navtoggle-demo" class="navscrim" aria-hidden="true"></label>
  <div class="adminbody">
    <nav class="adminnav" aria-label="Console example">
      <div class="adminnav__head"><span class="theme-novapay vlogo"><svg class="vmark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="6" width="17" height="12" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><rect x="6" y="12" width="3.6" height="3" rx="0.6"/></svg><span class="wm"><span class="nm">nova</span><span class="sf">pay</span></span></span><label for="navtoggle-demo" class="btn btn--ghost btn--sm toggle44" title="Close menu"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></label></div>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="8" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="5" rx="1.5"/><rect x="13.5" y="11.5" width="7" height="9" rx="1.5"/><rect x="3.5" y="14.5" width="7" height="6" rx="1.5"/></svg><span>Dashboard</span></a>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4v16h16"/><path d="M8.5 16v-4M12.5 16V7.5M16.5 16v-6"/></svg><span>Analytics</span></a>
      <details class="navgroup" open>
        <summary class="navgroup__label"><svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3.5 8.5 4.5-8.5 4.5L3.5 8Z"/><path d="m3.5 12.5 8.5 4.5 8.5-4.5"/></svg><span>Operations</span><svg class="icon icon--sm navgroup__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
        <a class="navlink" href="side-navigation.html" aria-current="page">Transactions</a>
        <a class="navlink" href="side-navigation.html">Data grid</a>
        <a class="navlink" href="side-navigation.html">Terminals</a>
      </details>
      <details class="navgroup">
        <summary class="navgroup__label"><svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h9m4 0h3M4 12h3m4 0h9M4 18h11m4 0h1"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="18" r="2"/></svg><span>Configuration</span><svg class="icon icon--sm navgroup__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
        <a class="navlink" href="side-navigation.html">Settings</a>
      </details>
    </nav>
    <div class="adminmain"><p class="muted" style="margin:0">Screen content. The menu button collapses the navigation at desktop width; on a phone it opens the navigation as a drawer.</p></div>
  </div>
</div>
live monitoring

Screen content. The chevron on the sidebar's edge collapses it; on a phone the appbar menu button takes over, as it does for every sidebar.

Flat sidebar (NovaPulse): peers with no grouping, so the toggle is the chevron on its edge
<div class="adminwrap" style="border:1px solid var(--border);border-radius:var(--radius-md)">
  <input type="checkbox" id="navtoggle-flat" class="navtoggle__input" aria-label="Show or hide the menu">
  <header class="appbar">
    <div class="appbar__inner appheader">
      <div class="appheader__left">
        <label for="navtoggle-flat" class="btn btn--ghost btn--sm toggle44 navtoggle" title="Menu"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></label>
        <span class="theme-novapay vlogo"><svg class="vmark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 14.5h4l2.5-7 4 12 2.5-5h5"/></svg><span class="wm"><span class="nm">nova</span><span class="sf">pulse</span></span></span>
        <span class="muted appsuffix">live monitoring</span>
      </div>
      <div class="appheader__right"><span class="appheader__endorse"><span class="brandmark" role="img" aria-label="Novus Technologies"></span></span></div>
    </div>
  </header>
  <label for="navtoggle-flat" class="navscrim" aria-hidden="true"></label>
  <div class="adminbody">
    <nav class="adminnav adminnav--flat" aria-label="Monitoring example">
      <label for="navtoggle-flat" class="navtoggle--edge" title="Collapse the menu">
        <svg class="icon ic-collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg>
        <svg class="icon ic-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m10 6 6 6-6 6"/></svg>
        <span class="sr-only">Collapse the menu</span>
      </label>
      <a class="navlink" href="side-navigation.html" aria-current="page"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13h4l2.5-7 4 12 2.5-5h5"/></svg><span>Live view</span></a>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v6m0 6v6M4.5 7.5 9 10m6 4 4.5 2.5M4.5 16.5 9 14m6-4 4.5-2.5"/></svg><span>Signals</span></a>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v8m0 0 3.5-3.5M12 12 8.5 8.5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg><span>Streams</span></a>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4.5h14v15H5z"/><path d="M8.5 9.5h7M8.5 13h4"/></svg><span>Runbooks</span></a>
      <a class="navlink" href="side-navigation.html"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v4l3 2"/></svg><span>History</span></a>
    </nav>
    <div class="adminmain"><p class="muted" style="margin:0">Screen content. The chevron on the sidebar's edge collapses it; on a phone the appbar menu button takes over, as it does for every sidebar.</p></div>
  </div>
</div>

Usage

  • Top-level links for the one or two screens people use most; everything else sits in groups named for an area, set in small caps with one line icon and a chevron.
  • The group holding the current page is open. The current link carries the accent tint, the accent edge, and aria-current="page".
  • One visually hidden checkbox drives the menu button: at desktop width it collapses the navigation to a rail of icons, and below 900px it opens the navigation as a drawer with a scrim that closes it. No JavaScript is required; the console script only closes the drawer after a link is followed and on Escape.
  • Collapsing never hides the navigation. The rail keeps one icon per destination at 44px, marks the current page with the accent tint, and names each destination through title and the label text, which stays in the markup for assistive technology. Press the menu button in the example above to see it.
  • Because of that rail, every destination carries an icon, including items inside groups. In the rail the group labels and chevrons step aside and their children show as icons, so nothing sits behind a collapsed group.
  • Labels are one to three words and never wrap. Rows are 36px on fine pointers and 44px on touch screens.

Choosing a sidebar

  • Grouped (NovaCard, the first example): when destinations belong to areas — Cards, Payments, Risk — the tree earns its keep, and the collapse control is the appbar hamburger, because the sidebar is one part of a wider header.
  • Flat (.adminnav--flat, the second example): when every destination is a peer and a group would invent a hierarchy that does not exist, the list is flat and the control is a chevron pinned to the sidebar's own edge. Collapsing hides labels only, so the button sits beside the thing it collapses instead of travelling to the other end of the window.
  • The two are the same mechanism: one hidden checkbox, the same rail, no JavaScript. Only the button and its icon change — a hamburger for the grouped tree, a chevron that flips direction with the state.
  • Below 900px both are drawers and the control returns to the appbar hamburger; the edge chevron is desktop-only and hides with the docked sidebar.

CSS

These rules ship in the console pattern layer, admin-kits/shared/novus-admin.css, which this site and every Admin Kit flavor load after tokens.css. Copy the whole layer, or only the sections below; every value is a token.

@pattern console-shell
.adminwrap { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
.demobar { background: var(--bg-subtle); border-bottom: 1px solid var(--border); padding: var(--space-1) var(--gutter); font-size: var(--text-xs); color: var(--text-secondary); text-align: right; }
.appbar__inner { max-width: none; font-size: var(--text-sm); line-height: var(--lh-sm); }
.appheader { flex-wrap: nowrap; }
.appheader__left { min-width: 0; gap: var(--space-2); }
.appheader__right { min-width: 0; }
.appsuffix { font-size: var(--text-sm); white-space: nowrap; }
.appname { font-weight: var(--weight-bold); font-size: var(--text-lg); color: var(--text); }
/* A 44px target, anywhere. Named for the rule it satisfies rather than the
   menu toggle it was first written for. .toggle44 is the former name and
   keeps working, so nothing already shipped breaks. */
.target44, .toggle44 { min-height: 2.75rem; min-width: 2.75rem; }
.navtoggle { padding: 0; cursor: pointer; }
.navtoggle__input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.navtoggle__input:focus-visible ~ .appbar .navtoggle { box-shadow: var(--focus-ring); }

/* Endorsement: placed asset via tokens, dark swap under BOTH triggers */
.brandmark { display: block; height: var(--logo-height); width: 132px; background: var(--logo-master-colour) center/contain no-repeat; flex: 0 0 auto; }
[data-theme="dark"] .brandmark { background-image: var(--logo-master-white); }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) .brandmark { background-image: var(--logo-master-white); } }
[data-theme="light"] .brandmark { background-image: var(--logo-master-colour); }

.adminbody { flex: 1; width: 100%; padding: var(--space-5) var(--gutter) var(--space-8); font-size: var(--text-sm); line-height: var(--lh-sm); }
.adminmain { min-width: 0; }
@media (min-width: 900px) {
  .adminbody { display: grid; grid-template-columns: 15rem minmax(0, 1fr); gap: var(--space-6); align-items: start; }
  /* Collapsed at desktop width: a rail of icons, never nothing (feature 009). */
  .adminwrap:has(.navtoggle__input:checked) .adminbody { grid-template-columns: 3.5rem minmax(0, 1fr); }
  .adminwrap:has(.navtoggle__input:checked) .adminnav { align-items: center; }
  .adminwrap:has(.navtoggle__input:checked) .navlink { justify-content: center; gap: 0; width: 2.75rem; padding: 0; border-inline-start: 0; border-radius: var(--radius-md); }
  .adminwrap:has(.navtoggle__input:checked) .navlink > span,
  .adminwrap:has(.navtoggle__input:checked) .navgroup__label > span,
  .adminwrap:has(.navtoggle__input:checked) .navgroup__chevron { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .adminwrap:has(.navtoggle__input:checked) .navgroup { border-top-color: transparent; }
  .adminwrap:has(.navtoggle__input:checked) .navgroup:last-child { border-bottom-color: transparent; }
  .adminwrap:has(.navtoggle__input:checked) .navgroup__label { justify-content: center; padding: 0; min-height: 0; height: var(--space-4); cursor: default; }
  .adminwrap:has(.navtoggle__input:checked) .navgroup__label .icon { display: none; }
  .adminwrap:has(.navtoggle__input:checked) .navgroup > .navlink { padding-inline-start: 0; }
  /* A closed group still shows its destinations in the rail: the browser stops painting a
     closed details subtree, so the rail asks for it back. Rail mode only, so the phone
     drawer keeps ordinary group behaviour. */
  .adminwrap:has(.navtoggle__input:checked) .navgroup::details-content { content-visibility: visible; block-size: auto; }
  .navscrim { display: none; }
}
@media (max-width: 899.98px) {
  .navscrim { position: fixed; inset: 0; z-index: 60; background: var(--scrim); opacity: 0; visibility: hidden; transition: opacity 0.2s ease, visibility 0.2s ease; }
  .adminwrap:has(.navtoggle__input:checked) .navscrim { opacity: 1; visibility: visible; }
  body:has(.navtoggle__input:checked) { overflow: hidden; }
}
/* Two-mark rule below 520px: divider and suffix drop, endorsement steps down, never disappears */
@media (max-width: 519.98px) {
  .appsuffix, .appheader__sep { display: none; }
  .brandmark { height: 20px; width: 65px; }
  .appbar__inner, .appheader__right { gap: var(--space-2); }
}

/* Icon theme toggle (moon in light, sun in dark), mirrored under both triggers */
.themetoggle .ic-sun { display: none; }
[data-theme="dark"] .themetoggle .ic-moon { display: none; }
[data-theme="dark"] .themetoggle .ic-sun { display: inline-block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .themetoggle .ic-moon { display: none; }
  :root:not([data-theme]) .themetoggle .ic-sun { display: inline-block; }
}
[data-theme="light"] .themetoggle .ic-moon { display: inline-block; }
[data-theme="light"] .themetoggle .ic-sun { display: none; }
@pattern side-navigation
.adminnav { display: flex; flex-direction: column; }
.adminnav__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-height: 3.5rem; padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4); margin-bottom: var(--space-2); border-bottom: 1px solid var(--border); }
@media (min-width: 900px) {
  .adminnav { position: sticky; top: var(--space-4); max-height: calc(100vh - var(--space-6)); overflow-y: auto; }
  .adminnav__head { display: none; }
}
@media (max-width: 899.98px) {
  .adminnav { position: fixed; inset-block: 0; inset-inline-start: 0; z-index: 70; width: min(20rem, 85vw); padding-bottom: var(--space-6); overflow-y: auto; background: var(--surface); border-inline-end: 1px solid var(--border); box-shadow: var(--shadow-3); transform: translateX(-100%); visibility: hidden; transition: transform 0.2s ease, visibility 0.2s ease; }
  .adminwrap:has(.navtoggle__input:checked) .adminnav { transform: none; visibility: visible; }
}
.navlink { display: flex; align-items: center; gap: var(--space-2); min-height: 2.25rem; padding: var(--space-2) var(--space-3); color: var(--text-secondary); text-decoration: none; white-space: nowrap; border-inline-start: 2px solid transparent; transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; }
.navlink:hover { color: var(--text); background: var(--bg-subtle); text-decoration: none; }
.navlink[aria-current="page"], .navlink.active { color: var(--accent-text); background: var(--accent-subtle); border-inline-start-color: var(--accent); font-weight: var(--weight-bold); }
.navlink:focus-visible, .navgroup__label:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.navgroup { border-top: 1px solid var(--border); }
.navgroup:last-child { border-bottom: 1px solid var(--border); }
.navgroup__label { list-style: none; display: flex; align-items: center; gap: var(--space-2); min-height: 2.25rem; padding: var(--space-2) var(--space-3); cursor: pointer; font-size: var(--text-xs); font-weight: var(--weight-regular); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-secondary); white-space: nowrap; transition: color 0.15s ease, background-color 0.15s ease; }
.navgroup__label::-webkit-details-marker { display: none; }
.navgroup__label > span { flex: 1; }
.navgroup__label:hover { color: var(--text); background: var(--bg-subtle); }
.navgroup__chevron { transition: transform 0.15s ease; }
.navgroup[open] > .navgroup__label .navgroup__chevron { transform: rotate(180deg); }
.navgroup > .navlink { padding-inline-start: calc(var(--space-3) + 1rem + var(--space-2)); }
@media (pointer: coarse) { .navlink, .navgroup__label { min-height: 2.75rem; } }
/* Variant: flat. For a product whose destinations are peers with no grouping to
   nest (a monitoring surface), the nav is one flat list. Its collapse control is
   a chevron pinned to the sidebar's own edge, not an appbar hamburger: collapsing
   hides only labels, so the button belongs beside the thing it collapses. It
   drives the same hidden checkbox, so every rail rule above applies unchanged. */
.adminnav--flat { position: relative; padding-block-start: var(--space-6); }
.navtoggle--edge { position: absolute; inset-block-start: var(--space-3); inset-inline-start: 100%; z-index: 5; translate: -50% 0; display: grid; place-items: center; width: 1.5rem; height: 1.5rem; padding: 0; border-radius: var(--radius-pill); background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-2); color: var(--text-secondary); cursor: pointer; }
.navtoggle--edge:hover { color: var(--text); }
.navtoggle--edge .icon { width: 1rem; height: 1rem; }
.navtoggle--edge .ic-expand { display: none; }
.adminwrap:has(.navtoggle__input:checked) .navtoggle--edge .ic-collapse { display: none; }
.adminwrap:has(.navtoggle__input:checked) .navtoggle--edge .ic-expand { display: block; }
@media (min-width: 900px) {
  .adminwrap:has(.adminnav--flat) .navtoggle { display: none; }
}
@media (max-width: 899.98px) {
  .navtoggle--edge { display: none; }
}