The Drill-Down Menu with Details and @scope

Preethi Sam Preethi Sam on

We’re going to build a set of nested <details> elements that behave like nested menus. Click a top-level link, drill down into a submenu, which can drill down into another submenu, etc. Think of them as nesting dolls… without the shrinking size. We take off a shell and get a new doll from inside.

This type of navigation pattern could be used to save space, be entertaining to use, or actually be helpful to users who know they need to follow a narrow path of questions to get where they need to go.

Here’s a live demo:

Let’s see how to make this one, starting with the layout.

The Nested Layout

A set of cascading, nested menus forms the foundation of this user interface. You can build it in your preferred way — you just need the nested <details> elements so users can open and close submenus. We’ll be targeting those states in CSS.

The <details> element is an interactive HTML element that can be opened and closed to show or hide additional content as needed, making it perfect for this drill-down UI.

The <summary> element within <details> becomes the clickable interactive element which opens all the other content within.

/* A three-level menu */

<details> /* Level 1 */
  <summary>Data Settings</summary>
  <details> /* Level 2 */
    <summary>Data Roaming</summary>
    <details> /* Level 3 */
      <summary>Domestic Roaming</summary>
      <!-- content -->
    </details>
    <!-- More in Level 3 -->
  </details>
  <!-- More in Level 2 -->
</details>
<!-- More in Level 1 -->Code language: HTML, XML (xml)

The Donut Scope

The two-state <details>, by itself, can show and hide its content, but when one of the <details> is open, the rest of them on the same level should not be visible. Although there are other ways to reach those <details>s to be removed, a very easy way to make it happen is by using @scope in CSS.

The @scope rule in CSS allows us to search for elements in a subtree defined by a scope root (an ancestor element) and a scope limit (a descendant element). This is similar to the limits of integration in calculus, where calculations are performed within a specific range defined by upper and lower bounds.

The starting element of your subtree is like the upper bound (scope root). The ending element, a descendant, is like the lower bound (scope limit). The scope limit is optional and unaffected by style rules. Only the scope root and elements between it and the limit are affected.

Here’s an example:

<main>
  <h1>Legal Provisions for Account Security</h1>
  <section>
    <h2>Section 4.1: User Accountability...</h2>
    <p>The User is solely and exclusively...</p>
  </section>
  <section>
    <h2>Section 4.2: Notification of...</h2>
    <p>In the event of an actual or suspected breach...</p>
  </section>
  <section class="plain-language-summary">
    <h2>Plain Language Summary</h2>
    <p>We want to keep your account safe, so you...</p>
    <p class="legal-clause">Legal Clause 4.1: User..</p>
    <p>If you lose access to your account...</p>
  </section>
</main>Code language: HTML, XML (xml)
main {
  font: 0.8rem monospace;
}
@scope (.plain-language-summary) to (.legal-clause) {
  * {
    color: navy;
    font-family: poppins;
  }
}
Code language: CSS (css)

The above document contains legal clauses styled in monospace font and default black color. A ‘plain language summary’ in poppins font and navy color outlines the document. The summary includes a legal clause that must retain the default monospace font and black color of the legal clauses.

Instead of having to reset the legal clause’s style inside the summary, @scope is used to style the .plain-language-summary's (scope root) content while excluding any .legal-clausees (scope limit) in it.

Being able to punch away elements not needed inward and outward — hence the term ‘donut’ — is what we need for the drill-down menu. When an inner menu opens, its outer menu and everything between the outer and inner one has to be erased, leaving only the open inner menu’s content on the screen.

@scope (:has(>details[open])) to (details[open]) {
  * {  /* or details, summary { */
    display: none;
  }
}Code language: CSS (css)

The outer menu with the open inner menu (:has(>details[open])) is the scope root. The open inner menu (details[open]) is the scope limit. All the <details> and <summary> elements between them are removed. Here’s the example once more:

Want to expand your CSS skills?

Leave a Reply

Your email address will not be published. Required fields are marked *

$966,000

Frontend Masters donates to open source projects through thanks.dev and Open Collective, as well as donates to non-profits like The Last Mile, Annie Canons, and Vets Who Code.