Dropdown

A Tailwind CSS dropdown component for displaying a list of actions or options.

<button
  class="btn"
  type="button"
  data-sp-toggle="dropdown"
  data-sp-target="#dropdown-options"
  aria-expanded="false"
>
  Options
</button>
<div id="dropdown-options" class="dropdown">
  <a href="#" class="dropdown-item">New file</a>
  <a href="#" class="dropdown-item">Copy link</a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item">Edit</a>
  <a href="#" class="dropdown-item">Rename</a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item">Delete</a>
</div>

With icons

Add icons to dropdown items for better visual hierarchy.

<button
  class="btn"
  type="button"
  data-sp-toggle="dropdown"
  data-sp-target="#dropdown-icons"
  aria-expanded="false"
>
  Options
</button>
<div id="dropdown-icons" class="dropdown">
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M9 15h6"/><path d="M12 18v-6"/></svg>
    New file
  </a>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
    Copy link
  </a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>
    Edit
  </a>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>
    Rename
  </a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
    Delete
  </a>
</div>

With labels

Use .dropdown-label to group items with a heading.

<button
  class="btn"
  type="button"
  data-sp-toggle="dropdown"
  data-sp-target="#dropdown-account"
  aria-expanded="false"
>
  My Account
</button>
<div id="dropdown-account" class="dropdown">
  <div class="dropdown-label">Settings</div>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
    Profile
  </a>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
    Billing
  </a>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>
    Notifications
  </a>
  <div class="dropdown-separator"></div>
  <div class="dropdown-label">Help</div>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
    Documentation
  </a>
  <a href="#" class="dropdown-item">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"/></svg>
    Support
  </a>
</div>

Disabled items

Use aria-disabled="true" to disable an item. Add tabindex="-1" to prevent tab navigation to the item.

<button
  class="btn"
  type="button"
  data-sp-toggle="dropdown"
  data-sp-target="#dropdown-disabled"
  aria-expanded="false"
>
  Actions
</button>
<div id="dropdown-disabled" class="dropdown">
  <a href="#" class="dropdown-item">Edit</a>
  <a href="#" class="dropdown-item" aria-disabled="true" tabindex="-1">
    Duplicate
  </a>
  <a href="#" class="dropdown-item">Archive</a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item" aria-disabled="true" tabindex="-1">
    Delete
  </a>
</div>

Placement

Use data-sp-placement on the trigger to control where the menu appears relative to it (default is bottom-end).

<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-bottom-start" data-sp-placement="bottom-start" aria-expanded="false">bottom-start</button>
<div id="dropdown-bottom-start" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-bottom" data-sp-placement="bottom" aria-expanded="false">bottom</button>
<div id="dropdown-bottom" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-bottom-end" data-sp-placement="bottom-end" aria-expanded="false">bottom-end</button>
<div id="dropdown-bottom-end" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-top-start" data-sp-placement="top-start" aria-expanded="false">top-start</button>
<div id="dropdown-top-start" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-top" data-sp-placement="top" aria-expanded="false">top</button>
<div id="dropdown-top" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-top-end" data-sp-placement="top-end" aria-expanded="false">top-end</button>
<div id="dropdown-top-end" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-left-start" data-sp-placement="left-start" aria-expanded="false">left-start</button>
<div id="dropdown-left-start" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-left" data-sp-placement="left" aria-expanded="false">left</button>
<div id="dropdown-left" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-left-end" data-sp-placement="left-end" aria-expanded="false">left-end</button>
<div id="dropdown-left-end" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-right-start" data-sp-placement="right-start" aria-expanded="false">right-start</button>
<div id="dropdown-right-start" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-right" data-sp-placement="right" aria-expanded="false">right</button>
<div id="dropdown-right" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-right-end" data-sp-placement="right-end" aria-expanded="false">right-end</button>
<div id="dropdown-right-end" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>

Offset

Use data-sp-offset on the trigger to control the distance between the trigger and the menu (default is 4).

<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-offset-0" data-sp-offset="0" aria-expanded="false">No Offset</button>
<div id="dropdown-offset-0" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-offset-default" aria-expanded="false">Default (4)</button>
<div id="dropdown-offset-default" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-offset-12" data-sp-offset="12" aria-expanded="false">Offset 12</button>
<div id="dropdown-offset-12" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
</div>

Hover trigger

Add data-sp-trigger="hover" to open the dropdown on pointer hover. Click still works for touch devices and keyboard users.

<button
  class="btn"
  type="button"
  data-sp-toggle="dropdown"
  data-sp-target="#dropdown-hover"
  data-sp-trigger="hover"
  aria-expanded="false"
>
  Hover me
</button>
<div id="dropdown-hover" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
  <a href="#" class="dropdown-item">Logout</a>
</div>

How it works

The dropdown component uses a small JavaScript module that handles opening, closing, positioning, and keyboard navigation.

Structure

A dropdown consists of two elements linked by id:

  1. [data-sp-toggle="dropdown"] with data-sp-target="#id" — the trigger button
  2. .dropdown with a matching id — the menu panel with items
<button data-sp-toggle="dropdown" data-sp-target="#my-menu" aria-expanded="false">
  Open Menu
</button>
<div id="my-menu" class="dropdown">
  <a href="#" class="dropdown-item">Item 1</a>
  <a href="#" class="dropdown-item">Item 2</a>
</div>

The trigger and the menu can live anywhere in the DOM as long as the id matches.

Opening and closing

Add data-sp-toggle="dropdown" and data-sp-target to the trigger to toggle the linked menu on click. Clicking outside the menu or pressing Escape closes it. Clicking an item also closes it by default.

For programmatic control, use the global StartingPointUI.dropdown module:

const trigger = document.querySelector("[data-sp-target='#my-menu']");
const menu = document.getElementById("my-menu");
 
StartingPointUI.dropdown.open(trigger);
StartingPointUI.dropdown.close(menu);
StartingPointUI.dropdown.toggle(trigger);

Animation

The dropdown includes a default fade and zoom animation. When opened, the JavaScript sets data-state="open" on the menu element.

To customize animations, add no-animation to disable the defaults and use your own classes with data-[state=open]: selectors:

<div
  id="my-menu"
  class="dropdown no-animation data-[state=open]:animate-in data-[state=open]:slide-in-from-top-2"
>
  <!-- Custom slide animation -->
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-anim-default" aria-expanded="false">Default</button>
<div id="dropdown-anim-default" class="dropdown">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
  <a href="#" class="dropdown-item">Logout</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-anim-none" aria-expanded="false">No Animation</button>
<div id="dropdown-anim-none" class="dropdown no-animation">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
  <a href="#" class="dropdown-item">Logout</a>
</div>
<button class="btn" type="button" data-sp-toggle="dropdown" data-sp-target="#dropdown-anim-slide" aria-expanded="false">Slide from Top</button>
<div id="dropdown-anim-slide" class="dropdown no-animation data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-2">
  <a href="#" class="dropdown-item">Profile</a>
  <a href="#" class="dropdown-item">Settings</a>
  <a href="#" class="dropdown-item">Logout</a>
</div>

The default animations use tw-animate-css utilities. You can customize or replace these with your own animations.

Accessibility

The dropdown provides full keyboard navigation and manages the aria-expanded state at runtime. Add aria-expanded="false" to the trigger button. The JavaScript toggles it to "true" when the menu opens:

<button data-sp-toggle="dropdown" data-sp-target="#my-menu" aria-expanded="false">
  Open
</button>

The dropdown does not add role="menu" or role="menuitem" by default. Dropdowns are used for many things in practice (actions, navigation links, search inputs, forms) and those patterns don't all fit the strict ARIA menu model. If your dropdown is exclusively a list of command actions (Edit, Delete, etc.), you can opt into true menu semantics by adding the roles yourself:

<div id="my-menu" class="dropdown" role="menu">
  <a href="#" class="dropdown-item" role="menuitem">Edit</a>
  <a href="#" class="dropdown-item" role="menuitem">Delete</a>
</div>

Keyboard navigation

KeyAction
Enter / SpaceOpens/closes dropdown when trigger is focused
EscapeCloses the dropdown
ArrowDownMove focus to next item
ArrowUpMove focus to previous item
HomeMove focus to first item
EndMove focus to last item
TabCycle focus through items inside the menu

Class reference

All available classes for the dropdown component.

ClassDescription
dropdownThe menu panel that appears on toggle
dropdown-itemIndividual menu item
dropdown-labelLabel/heading for grouping items
dropdown-separatorHorizontal divider between items
no-animationDisables default animation on the menu
<button data-sp-toggle="dropdown" data-sp-target="#my-menu">Open</button>
<div id="my-menu" class="dropdown">
  <div class="dropdown-label">Group</div>
  <a href="#" class="dropdown-item">Item</a>
  <div class="dropdown-separator"></div>
  <a href="#" class="dropdown-item">Item</a>
</div>

Data attributes

All data attributes for the dropdown component.

AttributeElementDescription
data-sp-toggle="dropdown"ButtonMarks the trigger
data-sp-target="#id"ButtonThe id of the linked .dropdown menu
data-sp-triggerButton"click" (default) or "hover"
data-sp-placementButtonMenu position (default: bottom-end)
data-sp-offsetButtonDistance from trigger in pixels (default: 4)
aria-expandedButtonSet "false" initially; JS toggles on open/close
data-state.dropdownSet to open when menu is visible
aria-disabled="true"ItemDisables the item visually and functionally
<button
  data-sp-toggle="dropdown"
  data-sp-target="#my-menu"
  data-sp-placement="bottom-start"
  data-sp-offset="8"
  aria-expanded="false"
>
  Open
</button>
<div id="my-menu" class="dropdown">
  <a href="#" class="dropdown-item">Enabled</a>
  <a href="#" class="dropdown-item" aria-disabled="true">Disabled</a>
</div>