Source Code

<div class="wrap">
  <div class="table-head">
    <h2 class="table-title">Projects</h2>
  </div>
  <div class="table-scroll">
    <table class="tbl">
      <thead>
        <tr>
          <th>Project</th>
          <th>Owner</th>
          <th>Status</th>
          <th>Updated</th>
          <th class="menu-col"></th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
  </div>
  <div class="toast" id="toast"></div>
</div>

Table with Row Action Dropdown — Free HTML CSS JS Snippet

Table with Row Action Dropdown · Tables · Plain HTML, CSS & JS · Live preview

What's included

Features

Single openMenuId tracks at most one open dropdown across the entire table at any time
Document-level click listener closes any open menu on an outside click
Escape key closes the open menu from anywhere on the page
Every action reads the current project object fresh by id, keeping dynamic labels (Pause/Resume) always accurate
Archive fades the row via a CSS class before removing it from the data array, avoiding an instant jarring disappearance
Shared toast notification confirms every action without blocking further interaction like a modal would
data-action attributes keep the menu template and the action-handling logic cleanly decoupled
ARIA aria-haspopup and aria-expanded attributes on the toggle button for assistive technology

About this UI Snippet

Table with Row Action Dropdown — Per-Row Kebab Menu, Single-Open State & Click-Outside Close

Screenshot of the Table with Row Action Dropdown snippet rendered live

Cramming five action buttons into every row of a table makes the table unreadable at a glance and wastes horizontal space on actions most rows won't need at any given moment. The kebab-menu pattern — a single small "⋯" button per row that opens a dropdown of contextual actions — keeps the table scannable while still surfacing rename, pause, duplicate, and archive actions exactly where they apply.

One open menu at a time, tracked by id

Rather than a boolean per row, the whole table tracks a single openMenuId value. Clicking a row's toggle button compares its id against openMenuId: if that row's menu was already open, it closes; otherwise closeAllMenus() runs first (closing whatever else was open) before the new id is set. This guarantees exactly one dropdown can be open across the entire table at any time — two simultaneously open menus in a scrollable table would be visually confusing and could overlap each other.

Closing on outside click and Escape, not just re-toggling

A single document-level click listener checks whether the click landed on a toggle button, an action item, or neither. When it's neither — and specifically not inside .menu-dropdown itself — every open menu closes. A separate keydown listener closes the open menu on Escape. Both are standard expectations for any dropdown menu; without them, a menu that only closes when its own toggle is clicked again would trap users into re-finding the same tiny button just to dismiss it.

Actions read the current data, not a stale snapshot

Every action handler (handleAction) looks up the project fresh from the projects array by id rather than closing over the object as it existed when the row was rendered. This matters because the dropdown's own rendering depends on current status (the "Pause" item becomes "Resume" once a project is paused) — reading fresh data on each click keeps the label and the action it performs in sync even across multiple menu opens.

Archive as a fade-then-remove, not an instant delete

Clicking "Archive" first adds an .archiving class (dimming the row via opacity) and only removes the project from the array — via setTimeout — after that visual cue has had a moment to register. An item vanishing from a list the instant a menu item is clicked can read as the table glitching rather than the action succeeding; the brief fade confirms cause and effect before the row disappears.

A shared toast instead of a modal for every confirmation

Every action (rename, pause, duplicate, archive) reports its result through one shared toast() element at the bottom of the screen rather than a blocking modal or alert. This keeps rapid actions — like duplicating three rows in a row — from interrupting the workflow with a dialog to dismiss after each one.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the single openMenuId variable, combined with the document-level click listener's closest() checks, guarantees that at most one dropdown can ever be open at a time even as the table re-renders on every click. The same assistant can help optimize it — for instance asking whether the dropdown should reposition itself when it would overflow the viewport near the table's edges, or the bottom of a scrollable table. It's also useful for extending the table: ask it to add keyboard arrow-key navigation between menu items once a dropdown is open, add a confirmation step before the destructive Archive action, or wire the whole thing to a real backend API with optimistic updates. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a data table in plain HTML, CSS, and JavaScript where each row has its own per-row "kebab menu" (a small ⋯ button) that opens a dropdown of contextual actions — no library, no framework.

Requirements:
- Keep the table's data in a single array of plain objects and re-render the full table body from that array on every state change, the same render-from-array pattern any state-driven table should use.
- Track which row's dropdown is currently open using a single shared value (such as one row id, not a boolean flag per row), so that opening one row's dropdown always closes any other row's dropdown that might currently be open — never allow two dropdowns open simultaneously.
- Clicking anywhere outside an open dropdown (and outside its own toggle button) must close it, and pressing the Escape key must also close whichever dropdown is currently open, from anywhere on the page.
- Include at least four distinct actions in the dropdown (for example rename, toggle a status field, duplicate the row, and a destructive delete/archive action), where at least one action's label text changes dynamically based on that row's current data (for example a Pause action that becomes a Resume action once the row is already paused).
- The destructive action must not remove its row from the DOM instantly — first apply a brief visual transition (such as a fade or dim) to the row, then remove the underlying data and re-render only after that transition has had time to be seen.
- Confirm the result of every action (including the destructive one) via a single small shared toast/snackbar element that appears briefly and does not block further interaction with the table, rather than a blocking modal or native alert dialog.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click the ⋯ button on any rowOpens that row's dropdown with Rename, Pause/Resume, Duplicate, and Archive actions. Opening a new row's menu automatically closes any other open one.
  2. 2
    Click outside or press EscapeBoth close the currently open dropdown without taking any action.
  3. 3
    Try each actionRename prompts for a new name, Pause/Resume toggles status, Duplicate adds a copy, and Archive fades the row out before removing it — each shows a toast confirmation.
  4. 4
    Edit the menu itemsAdd or remove <button class="menu-item" data-action="..."> elements in the render() template string, then add a matching branch in handleAction().
  5. 5
    Replace the data-driven actions with real API callsInside each handleAction() branch, replace the local array mutation with a fetch call to your backend, then re-render once it succeeds.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Admin panels and content management tables
The classic use case — a projects, users, or posts table where each row needs a handful of contextual management actions without cluttering the row itself.
CRM and pipeline management tools
Add actions like "Move to stage," "Assign owner," or "Log activity" per contact or deal row.
File and document management tables
Offer Rename, Move, Download, and Delete per file row without needing a full toolbar for every single file.
Related: Editable Table
See the Editable Table for a related tables pattern worth pairing with this one — inline field editing alongside per-row actions.
Learn single-open-dropdown state management
The openMenuId pattern (tracking one open id instead of a boolean per row) is a reusable technique for any list where only one expandable element should be open at a time.

Got questions?

Frequently Asked Questions

The whole table tracks a single openMenuId variable rather than a per-row open flag. Clicking a toggle button calls closeAllMenus() (which clears openMenuId and removes every .open class) before conditionally setting openMenuId to the clicked row's id — so opening one row's menu always closes any other.

A single document-level click listener checks e.target.closest(".menu-dropdown") — if the click did not land inside an open dropdown (and was not the toggle button or a menu item, both handled earlier in the same listener), closeAllMenus() runs and the table re-renders.

The archive action first adds an .archiving class that dims the row via CSS opacity, then uses setTimeout to actually splice the project out of the array and re-render after 200ms. This gives the user a moment to see the row respond before it disappears, rather than an instant jump.

Add a new <button class="menu-item" data-action="export" data-id="${p.id}">Export</button> inside the dropdown template in render(), then add an else if (action === "export") { ... } branch inside handleAction() implementing the behavior.

The dropdown is positioned with right: 0 relative to its menu cell, so it aligns to the right edge of the button by default, which works well for a right-aligned action column. For a table where the action column is not the last column, you would need to add logic that flips the dropdown to open leftward when it would overflow the viewport.

Keep openMenuId in useState(null), toggle it in the button's onClick, and use a useEffect with a document click/keydown listener (cleaned up on unmount) to close it on outside click or Escape — the same logic as the vanilla JS version, just moved into React's state and effect model.