Side menu
The navigation list, and only that: it's the part that repeats the same way in every project. There is no component or JavaScript of its own — just classes on links. What goes in it, how it's grouped and where it lives is still your call: fixed on the side on desktop, inside a drawer on mobile, or both.
Click the items
The current item gets color, background and weight at the top level, and a neutral pill inside the submenu. Groups open and close with the accordion animation.
On mobile, inside a drawer
The same list in a drawer from the left — the usual pattern for an app menu on a small screen.
Examples
The pieces of the menu, one at a time.
Items and current item
is-active marks the page the person is on.
Icon and counter
__icon on the left; __count is the .tuc-badge is-plain, pushed to the right.
Sections and separator
__section is the label of a block; __separator, a line between blocks.
Submenu
A <details> with __sub; the chevron points right when closed and down when open.
How to use
A <nav> with the class and links inside. The data-tuc-accordion on the <nav>
is only needed when there's a submenu: it's what animates the <details>.
<nav class="tuc-menu" aria-label="Main" data-tuc-accordion>
<!-- simple item -->
<a class="tuc-menu__item" href="/billing/">
<span class="tuc-menu__icon"><svg>…</svg></span>Billing
</a>
<!-- item with counter -->
<a class="tuc-menu__item" href="/products/">
<span class="tuc-menu__icon"><svg>…</svg></span>Products
<span class="tuc-badge is-plain tuc-menu__count">12</span>
</a>
<!-- item with submenu -->
<details open>
<summary class="tuc-menu__item">
<span class="tuc-menu__icon"><svg>…</svg></span>Performance
</summary>
<div class="tuc-menu__sub">
<a class="tuc-menu__item is-active" href="/performance/period/" aria-current="page">By period</a>
<a class="tuc-menu__item" href="/performance/branches/">Branches</a>
</div>
</details>
<hr class="tuc-menu__separator">
<div class="tuc-menu__section">Records</div>
<a class="tuc-menu__item" href="/customers/">Customers</a>
</nav>In the Django template
The current item comes from the resolved route. Write the menu once in an include and it marks the right page everywhere:
{% with route=request.resolver_match.url_name %}
<nav class="tuc-menu" aria-label="Main" data-tuc-accordion>
<a class="tuc-menu__item{% if route == 'contract-list' %} is-active{% endif %}"
href="{% url 'contract-list' %}"{% if route == 'contract-list' %} aria-current="page"{% endif %}>
Contracts
</a>
<details{% if route|slice:":12" == 'performance-' %} open{% endif %}>
<summary class="tuc-menu__item">Performance</summary>
<div class="tuc-menu__sub">
<a class="tuc-menu__item{% if route == 'performance-period' %} is-active{% endif %}"
href="{% url 'performance-period' %}">By period</a>
</div>
</details>
</nav>
{% endwith %}Leave the group containing the current page open in the template: whoever arrives through a submenu link already sees where they are.
In the sidebar and in the drawer
The menu doesn't know where it lives. On desktop, put it in a fixed column; on mobile, the same include goes inside a
drawer from the left:
<dialog class="tuc-drawer is-left is-sm" id="menu-main" aria-labelledby="menu-main-title">
<div class="tuc-drawer__panel">
<div class="tuc-drawer__top">
<div class="tuc-drawer__header">
<h2 class="tuc-drawer__title" id="menu-main-title">Dashboard</h2>
</div>
<button type="button" class="tuc-btn is-ghost is-icon is-sm tuc-drawer__close" aria-label="Close" data-tuc-drawer-close>…</button>
</div>
<div class="tuc-drawer__body">
{% include "partials/menu.html" %}
</div>
</div>
</dialog>
<button class="tuc-btn is-outline is-icon" aria-label="Open menu" data-tuc-drawer="#menu-main">…</button>In the fixed desktop column, make it a flex column with a height, with the brand on top and the menu below. The
.tuc-menu takes the remaining space and scrolls on its own when the list is taller than the screen — the
brand stays put at the top. In the drawer, the drawer body is what scrolls, and nothing else is needed.
<aside style="display: flex; flex-direction: column; height: 100vh; position: sticky; top: 0">
<a href="/">My brand</a>
<nav class="tuc-menu" aria-label="Dashboard">…</nav>
</aside>Submenu
The item that opens and reveals children is a <details>, and opening reuses the
accordion: the animation, the keyboard and working without JavaScript come ready. The menu only takes care
of the reading — who is a child of whom, and where the current item sits in the tree.
The <summary> takes tuc-menu__item and looks just like its neighbors, with the label aligned to the
left and the chevron pushed to the right. Children go in tuc-menu__sub, indented and attached to a rail on the
left. A child doesn't repeat the icon: it inherits the parent's context, and the dot in front marks the row without competing
for attention with the row of icons above.
The chevron turns 90 degrees, not 180
Right when closed, down when open — that's how a navigation tree reads: "there's something in here" versus "this is the open branch". And the rail on the left exists because indentation alone gets lost as soon as the list goes past four or five rows.
The current item carries three cues
At the top level, accent color, background and weight — none of them relies on color alone. Inside the submenu the current item is a neutral pill with the dot lit: the accent color stays with the level above, and two orange items in the same column would say less.
Keyboard and accessibility
They're links and <details>, and the keyboard is theirs: nothing to learn, nothing to implement.
| Key | Action |
|---|---|
Tab Shift+Tab | Moves between items, including submenu titles; children of a closed group stay out of the path |
Enter | Follows the link, or opens and closes the focused submenu |
Space | Opens and closes the focused submenu |
Write aria-current="page" next to is-active: the class draws, the attribute is what the screen reader
announces. Give the <nav> an aria-label when there's more than one navigation on the page. And don't use
role="menu" here: that role promises the arrow keys of an application menu, and this list is plain navigation.
Classes
Classes only — the menu has no JavaScript and no options. What animates the submenu is the accordion.
| Class or attribute | Where | What for |
|---|---|---|
tuc-menu | <nav> | The list, in a column |
tuc-menu__item | <a> or <summary> | One menu row, with the height of a control |
is-active | .tuc-menu__item | The current item: highlight at the top level, neutral pill in the submenu |
tuc-menu__icon | <span> in the item | 16px box for the icon, in the secondary color; on the active item, in the accent color |
tuc-menu__count | .tuc-badge is-plain in the item | Pushes the counter to the right, with fixed-width digits |
tuc-menu__separator | <hr> | Line between blocks |
tuc-menu__section | <div> | Label of a block, in uppercase |
tuc-menu__sub | <div> inside the <details> | The submenu children, indented and with a rail |
data-tuc-accordion | <nav> | Animates submenus opening; data-single="true" keeps one open at a time |