webdevpuneet.com

Data Table Column Toggle — Show/Hide Columns UI

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

CategoryTables

Copy & paste

Source Code

<div class="dct-card">
  <div class="dct-head">
    <h3>Team members</h3>
    <div class="dct-cols-wrap" id="dctWrap">
      <button type="button" class="dct-cols-btn" id="dctBtn" aria-haspopup="true" aria-expanded="false">
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M3 12h18M3 19h18"/></svg>
        Columns <span class="dct-count" id="dctCount"></span>
      </button>
      <div class="dct-menu" id="dctMenu" role="menu"></div>
    </div>
  </div>

  <div class="dct-table-wrap">
    <table class="dct-table" id="dctTable"></table>
  </div>
</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

Data-driven columns
Columns come from a COLUMNS array; the table is always a clean projection of the visible ones — no index-based <td> hiding.
Locked always-on column
A locked column stays checked, disabled, and always rendered, so the row-identifying column can never be hidden.
Checklist picker dropdown
A "Columns" menu of checkboxes toggles each column's visibility with instant re-render.
Live visible-count badge
The button shows how many columns are visible without opening the menu.
Typed cell rendering
A cell() function renders special columns (like a status badge) while plain columns fall through to their value.
Accessible menu
aria-haspopup, toggled aria-expanded, outside-click dismissal, and opacity/transform-only animation.
Horizontal scroll fallback
An overflow-x wrapper keeps a wide table usable; column toggling is the primary tool for fitting small screens.
Persistence-ready
Visibility lives in the column config, so saving the visible keys to localStorage and restoring is a few lines.

About this UI Snippet

Data Table Column Toggle — Show/Hide Columns with a Picker Dropdown

Screenshot of the Data Table Column Toggle snippet rendered live

Dense data tables try to show everything, and on smaller screens or for focused tasks that's too much. Letting users pick which columns they see — a "Columns" dropdown of checkboxes — turns an overwhelming grid into one tailored to what each person actually needs. This snippet builds that column-toggle pattern in plain HTML, CSS, and vanilla JavaScript: a checklist dropdown, a locked always-visible column, and a table that re-renders to match.

Columns as data, visibility as state

The table is defined by a COLUMNS array, each entry carrying a key, a label, and a visibility flag (plus an optional locked). renderTable() filters that array to the currently-visible columns and builds the header and every row from the result — so showing or hiding a column is just flipping its visible flag and re-rendering. There's no fragile per-column DOM manipulation (hiding <td>s by index, which breaks the moment columns reorder); the table is always a clean projection of the column config, which is the robust way to do this.

A locked column that can't be turned off

Some columns are the table's anchor — usually the name or ID that identifies each row. Marking a column locked keeps its checkbox checked and disabled in the picker (shown as "Name (fixed)") and always includes it in the render, so a user can't accidentally hide the one column that makes the rest meaningful. This guard is what keeps the feature from producing a useless table of anonymous data.

The picker dropdown

A "Columns" button opens a dropdown of checkboxes, one per column, with a live count badge showing how many are currently visible. Toggling a checkbox updates that column's flag and immediately re-renders the table. The dropdown is a standard menu: it opens on click, closes on an outside click, animates in with opacity and transform only, and the button carries aria-haspopup and a toggled aria-expanded. The count badge gives instant feedback ("4 of 5 shown") without opening the menu.

Cell rendering that handles types

Not every column is plain text — a status column renders a colored badge, for instance. A small cell() function maps a column key to its rendered content, so special columns format correctly while the rest fall through to their raw value. This keeps the render loop generic (it doesn't care what a column contains) while still supporting rich cells, and it's the hook where you'd add formatting for dates, currency, or links.

Horizontal scroll and responsive intent

The table sits in an overflow-x: auto wrapper so that even with several columns shown it stays usable on narrow screens — and the whole point of column toggling is that users on small screens can hide what they don't need to avoid that scroll entirely. The two work together: the toggle is the primary tool for fitting the table to the viewport, and horizontal scroll is the graceful fallback when many columns are kept.

Persisting the user's choice

The visible-column set is exactly the kind of preference worth remembering. Because visibility lives in the COLUMNS config, persisting it is trivial — save the visible keys to localStorage on change and restore them on load — so a user's tailored view survives a refresh. The FAQs cover that plus per-user server-side persistence for logged-in apps.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA team-members table renders with all columns shown and a "Columns" button showing the visible count.
  2. 2
    Open the column pickerClick "Columns" to open a checklist of every column; Name is locked and shown as "(fixed)".
  3. 3
    Toggle columnsUncheck a column (e.g. Email) — it disappears from the table instantly and the count badge updates.
  4. 4
    Try the locked columnNote that Name can't be unchecked, so the table always keeps the column that identifies each row.
  5. 5
    Edit columns and dataChange the COLUMNS array (keys, labels, locked) and the ROWS data — the picker and table rebuild from them.
  6. 6
    Persist the choiceSave the visible column keys to localStorage on change and restore on load so the user's view survives a refresh.

Real-world uses

Common Use Cases

Admin dashboards and data grids
Let operators tailor a dense table to their task — pair with a resizable columns table for full control.
CRM and contact tables
Show or hide fields (email, phone, owner, last contact) based on what the user is working on.
Analytics and reporting tables
Toggle metric columns to focus a report, alongside a sortable table for ordering.
Project and task lists
Hide secondary columns (assignee, tags, due date) on smaller screens or for a cleaner view.
E-commerce and inventory tables
Pick which product attributes to display in a management grid.
Learning data-driven table rendering
A reference for projecting a column config into a table and a locked-column guard — compare with a pagination table for paging.
Related: Table Density Toggle
See the Table Density Toggle for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Because visibility is stored on the COLUMNS config, save the visible keys (COLUMNS.filter(c => c.visible || c.locked).map(c => c.key)) to localStorage whenever a checkbox changes, and on load read them back and set each column's visible flag accordingly before the first renderTable(). For a logged-in app, store the preference server-side against the user so it follows them across devices.

Hiding cells by index (e.g. display:none on the nth <td>) is fragile — it breaks if columns reorder, and you must keep header and body indices in sync manually. Rendering both the header and rows from the same filtered column array means the table is always a correct projection of the config, with no index bookkeeping, which is far more robust as the table grows.

Mark them locked in the COLUMNS array — the picker renders their checkbox as checked and disabled, and renderTable() always includes locked columns regardless of the visible flag. This guarantees the row-identifying column (name, ID) is always present so the rest of the data stays meaningful.

For reordering, make the picker a drag-sortable list and reorder the COLUMNS array on drop — since the table renders from the array order, it reflects the new order automatically. For pinning a column to the left, render it in a separate sticky-positioned column or apply position: sticky to its cells; the visibility logic stays the same.

In React, hold the columns config (with visibility) in useState and derive the visible set with useMemo, rendering the table and picker from it; in Vue, use a reactive columns array with computed; in Angular, use a component array with getters. The filter-and-render approach is identical — only the per-toggle re-render moves into the framework's reactivity.