webdevpuneet.com

Table Column Visibility Toggle Menu — Real Dynamic Show/Hide Columns

Table Column Visibility Toggle Menu · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="demo">
  <div class="col-toolbar">
    <div class="col-menu-wrap">
      <button class="col-menu-btn" id="colMenuBtn" aria-haspopup="true" aria-expanded="false">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
        Columns
        <span class="col-count" id="colCount"></span>
      </button>
      <div class="col-menu" id="colMenu" role="menu">
        <label class="col-item"><input type="checkbox" data-col="email" checked /><span>Email</span></label>
        <label class="col-item"><input type="checkbox" data-col="role" checked /><span>Role</span></label>
        <label class="col-item"><input type="checkbox" data-col="status" checked /><span>Status</span></label>
        <label class="col-item"><input type="checkbox" data-col="lastActive" checked /><span>Last active</span></label>
        <label class="col-item"><input type="checkbox" data-col="joined" /><span>Joined date</span></label>
      </div>
    </div>
  </div>

  <table class="col-table" id="colTable">
    <thead>
      <tr>
        <th>Name</th>
        <th data-col="email">Email</th>
        <th data-col="role">Role</th>
        <th data-col="status">Status</th>
        <th data-col="lastActive">Last active</th>
        <th data-col="joined">Joined date</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Maya Chen</td>
        <td data-col="email">[email protected]</td>
        <td data-col="role">Admin</td>
        <td data-col="status"><span class="badge on">Active</span></td>
        <td data-col="lastActive">2 hours ago</td>
        <td data-col="joined">Jan 12, 2024</td>
      </tr>
      <tr>
        <td>Rahul Patel</td>
        <td data-col="email">[email protected]</td>
        <td data-col="role">Editor</td>
        <td data-col="status"><span class="badge on">Active</span></td>
        <td data-col="lastActive">1 day ago</td>
        <td data-col="joined">Mar 4, 2024</td>
      </tr>
      <tr>
        <td>Ines Fischer</td>
        <td data-col="email">[email protected]</td>
        <td data-col="role">Viewer</td>
        <td data-col="status"><span class="badge off">Inactive</span></td>
        <td data-col="lastActive">3 weeks ago</td>
        <td data-col="joined">Jun 20, 2024</td>
      </tr>
    </tbody>
  </table>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Genuine show/hide — a single data-col attribute links each checkbox to both its header cell and every row's matching cell
One toggle always hides or shows an entire column consistently, never leaving header and body cells out of sync
One always-visible identifying column structurally exempt from the hide/show system, keeping the table always meaningful
Visible-column counter badge correctly accounts for the always-visible column in its displayed ratio
Standard dropdown menu conventions — aria-expanded, click-outside-to-close — matching this library's other menu patterns
Single querySelectorAll per column toggle efficiently updates every affected cell in one pass
Works with any number of columns and rows without additional wiring per row
Pure vanilla JavaScript — no table/grid library dependency

About this UI Snippet

Table Column Visibility Toggle — Actually Adding and Removing Columns

Screenshot of the Table Column Visibility Toggle Menu snippet rendered live

A "columns" menu is only useful if unchecking a box genuinely removes that column from the table — header and every row's matching cell together — rather than just visually greying out a menu item with no real effect. This snippet implements the real behavior: each checkbox is wired to a shared data-col identifier that both the header cell and every body cell in that column carry, so one toggle hides or reveals an entire column consistently across the whole table.

One data attribute links a checkbox to its entire column, header included

Every column that can be hidden has a data-col value — email, role, status, and so on — applied to *both* its <th> and every <td> in that column across every row. The checkbox controlling it carries the same value in its own data-col attribute. applyVisibility() reads each checkbox's data-col, then runs a single table.querySelectorAll([data-col="${col}"]) scoped to the whole table — which matches the header cell *and* every row's cell for that column in one query, applying the same .col-hidden toggle to all of them together, so a column can never end up with its header hidden but its body cells still showing, or vice versa.

The "Name" column is intentionally not toggleable

The first column (Name) has no data-col attribute and no matching checkbox — it's structurally exempt from the hide/show system entirely, modeling the realistic constraint that a table needs at least one identifying column always visible to remain meaningful; hiding every other column while keeping Name visible still leaves a usable, if minimal, table.

The visible-column counter accounts for that always-visible column explicitly

colCount.textContent computes ${visibleCount + 1}/${totalCount + 1} — adding 1 to both the numerator and denominator specifically to account for the always-visible Name column, which isn't represented in the checkboxes array at all. Getting this off-by-one right matters for the badge to honestly report "6/6" when everything is visible, rather than under-reporting "5/5" and omitting the one column that was never optional in the first place.

Dropdown menu behavior follows the same conventions as this library's other menus

The columns button toggles aria-expanded and a .open class on the dropdown, and a document-level click listener closes the menu whenever a click lands outside .col-menu-wrap — standard, predictable dropdown-menu behavior that doesn't require re-learning for anyone who's used a similar menu elsewhere in a product.

Step by step

How to Use

  1. 1
    Click "Columns" and toggle any checkboxWatch the corresponding column's header and every row's cell for that column hide or reveal together, immediately.
  2. 2
    Add a new toggleable columnGive the new <th> and every matching <td> the same new data-col value, and add a matching checkbox with that same value in the menu.
  3. 3
    Keep an identifying column always visibleLeave your primary identifying column (like Name here) without a data-col attribute so it's structurally exempt from being hidden.
  4. 4
    Persist the visibility state (optional)Save each checkbox's checked state to localStorage on change, and restore it before the initial applyVisibility() call, so a user's column preferences survive a reload.
  5. 5
    Adjust which columns start hidden by defaultRemove the checked attribute from any checkbox in the HTML to have that column start hidden.

Real-world uses

Common Use Cases

ADMIN
Admin Data Tables
Let users customize which fields they see in a dense admin table of users, orders, or records.
SAAS
Customizable Dashboard Tables
Give users control over information density in any data-heavy table view.
CRM
CRM / Spreadsheet-Style Views
A common pattern in CRM and spreadsheet-like tools where users tailor visible columns to their workflow.
REPORTING
Report Builder Tables
Let users toggle which metrics or dimensions appear in a generated report table.
Related: Table Loading / Empty / Error States
See the Table Loading / Empty / Error States for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It genuinely hides the column — both the header cell and every row's corresponding cell get display:none applied via a shared .col-hidden class, driven by a single data-col attribute shared between the checkbox and every cell in that column, so the column's space is fully reclaimed by the table layout, not just dimmed.

No — both are toggled together in the exact same operation, since applyVisibility() queries table.querySelectorAll([data-col="${col}"]) scoped to the whole table, which matches the header cell and every row's cell for that column in one pass and applies the identical class change to all of them.

It deliberately has no data-col attribute and no matching checkbox, modeling the realistic requirement that a table keeps at least one identifying column always visible — hiding every other column should still leave a minimally usable table, not an empty one.

The counter explicitly adds 1 to both the visible count and the total count to account for the always-visible Name column, which has no checkbox representing it at all — without that adjustment, the badge would misleadingly under-report the true number of visible and total columns.

Add a data-col attribute with a new unique value to the new column's <th> and to every row's matching <td>, then add a corresponding checkbox with that same data-col value inside the menu — applyVisibility() picks it up automatically since it iterates over whatever checkboxes currently exist in the menu.

Not by default in this snippet — checkbox states reset to their HTML-defined defaults on reload. Add localStorage reads/writes around each checkbox's checked state if you want a user's column preferences to persist.