Tucano v0.37.2

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>

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.

KeyAction
Tab Shift+TabMoves between items, including submenu titles; children of a closed group stay out of the path
EnterFollows the link, or opens and closes the focused submenu
SpaceOpens 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 attributeWhereWhat 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__itemThe current item: highlight at the top level, neutral pill in the submenu
tuc-menu__icon<span> in the item16px 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 itemPushes 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