Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Dropdown menu and tabs

Dropdown menus and tabs appear on almost every site. With Alpine both are built from directives you already know: one property in x-data, x-show for visibility and @click for switching. Both pieces are part of the to-do page in index.html.

A dropdown menu

A menu of actions for a task. The minimal version is a button and a list that opens on click:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 
<div x-data="{ open: false }">
  <button @click="open = !open">Actions</button>
  <ul x-show="open">
    <li>Edit</li>
    <li>Delete</li>
  </ul>
</div>

The button opens and closes the list. But so far the menu closes only on another click of the button.

Closing on an outside click and Esc

Users expect a menu to close when they click anywhere else and when they press Esc:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 
<div x-data="{ open: false }" @keydown.escape.window="open = false" style="position: relative; display: inline-block">
  <button @click="open = !open" :aria-expanded="open">Actions ▾</button>
 
  <ul
    x-show="open"
    x-transition.opacity
    @click.outside="open = false"
    style="position: absolute; margin: 4px 0; padding: 4px 12px; border: 1px solid #ccc; background: Canvas; list-style: none"
  >
    <li><button @click="console.log('Edit'); open = false">Edit</button></li>
    <li><button @click="console.log('Delete'); open = false">Delete</button></li>
  </ul>
</div>

The menu closes on a click outside it, on Esc and after an item is chosen, and the chosen action appears in the console. The aria-expanded attribute tells screen readers whether the menu is open.

Tabs

Tabs keep the name of the active tab. A button changes it, and a panel is shown when the name matches:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 
<style>
  .tab-active { font-weight: bold; text-decoration: underline; }
</style>
 
<div x-data="{ tab: 'active' }">
  <nav>
    <button @click="tab = 'active'" :class="{ 'tab-active': tab === 'active' }">Active</button>
    <button @click="tab = 'done'" :class="{ 'tab-active': tab === 'done' }">Done</button>
  </nav>
 
  <div x-show="tab === 'active'">
    <p>Buy milk</p>
    <p>Call Oleg</p>
  </div>
  <div x-show="tab === 'done'">
    <p>Pay for internet</p>
  </div>
</div>

The "Active" tab with two tasks is open first. The "Done" button switches the panel and highlights itself in bold.

When there are many tabs, describe them as an array and render the buttons with x-for — then a new tab is one more line of data.

Common mistake

Writing @keydown.escape without .window. A keyboard event goes to the element that has focus. In Safari a click on a button does not focus it: the menu is open, focus stays on the <body>, and Esc without .window closes nothing. The .window modifier listens for the key on the whole window, so the menu always closes.

Thanks for reading!

Donate to the site — we work hard for you.