webdevpuneet.com

Editable Table — Free HTML CSS JS Inline Edit Snippet

Editable Table · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="wrap">
  <div class="table-head">
    <h2 class="table-title">Team Directory</h2>
    <div class="table-actions">
      <button class="add-row-btn" id="add-row-btn">+ Add row</button>
    </div>
  </div>
  <div class="table-scroll">
    <table class="tbl" id="tbl">
      <thead>
        <tr>
          <th>Name</th>
          <th>Role</th>
          <th>Department</th>
          <th>Email</th>
          <th class="del-col"></th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
  </div>
  <div class="table-foot">
    <span id="row-count">4 rows</span>
    <button class="save-btn" id="save-btn">Save changes</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

All cells: transparent input that shows indigo focus border on click-to-edit
Hover border: faint #e2e8f0 border on .cell-input:hover for edit affordance
Data binding: oninput writes rows[i][key] for instant sync without re-render
Add row: push empty object, re-render, add .new-row animation class, focus first input
Delete row: splice(i,1) + re-render — no DOM tracking needed
new-row animation: slide-in from translateY(-4px) + opacity 0 in 0.2s
Row count footer: updates on every render from rows.length
Save: collects all rows, filters empty, ready for API POST

About this UI Snippet

Editable Table — Inline Cell Inputs, Add Row Animation, Delete & Save All Changes

Screenshot of the Editable Table snippet rendered live

An editable table lets users modify data directly in a table without navigating to a separate form. It is the standard pattern for spreadsheet-style data editing in admin panels, CRM tools, project management apps, and any interface where users need to edit multiple records at once. This snippet provides a complete inline editable table: all four column cells use text inputs that are transparent by default and show a focus border on edit, plus add row with animation, delete row with a hover-reveal × button, row count in the footer, and a save button.

How the inline cell editing works

The table is rendered entirely from a rows array. Each cell contains an input element (type="text" or type="email") styled to be invisible by default — transparent border and background that match the table cell. On hover, a faint border appears. On focus (click to edit), a full indigo border appears with a soft box-shadow ring. This creates the impression of plain text that transforms into an editable field on interaction — the standard inline edit pattern.

The data binding

Each input has an oninput handler that writes back to rows[i][key] — rows is a flat array of objects. The render() function reads from this array and re-creates the full tbody on each change. This simple re-render approach means the data is always in sync without complex two-way binding.

Add row with animation

addRow() pushes an empty row object to the array, calls render(), then selects the last row in the tbody and adds the .new-row class. This class applies a slide-in keyframe animation and a light indigo background tint. The first input of the new row receives focus automatically so the user can start typing immediately.

Delete row

The × button calls rows.splice(i, 1) to remove the row at index i, then re-renders. The entire table re-renders from the updated array — simple and reliable without needing to track DOM elements.

Save all changes

The saveData() function collects all current row values from the rows array. It filters out completely empty rows (no name). In production, replace the alert with a POST or PUT fetch call to your API endpoint.

Customising the columns

The four columns (name, role, dept, email) are defined in the ['name','role','dept','email'] array passed to forEach. Add or remove columns by updating this array and adding matching keys to the rows array objects. Update the thead th elements to match. Pair it with the Data Table snippet for read-only views and the Sortable Table for click-to-sort columns.

Step by step

How to Use

  1. 1
    Click any cell to edit inlineAll cells are transparent text inputs. Click to focus — an indigo border and focus ring appear, signalling edit mode. Type to change the value. Click outside to blur and save the change to the rows array.
  2. 2
    Click "+ Add row" to insert a new rowA new blank row slides in at the bottom of the table with a light indigo tint animation and the first cell auto-focused. Type to fill in the row data.
  3. 3
    Click × to delete a rowHover over any row to reveal the × delete button on the right. Click it to remove the row immediately. The row count in the footer updates.
  4. 4
    Click "Save changes" to collect all dataThe saveData() function collects all rows from the array. Replace the alert with a fetch POST to your API: fetch("/api/team", { method:"POST", body: JSON.stringify(rows) }).
  5. 5
    Add or remove columnsUpdate the ["name","role","dept","email"] array in render() and add matching headers in the thead. Also add the new key to the empty row object in addRow(): { name:"", role:"", dept:"", email:"", newCol:"" }.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState for rows and controlled inputs, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Team directory and employee data management
The four-column layout (Name, Role, Department, Email) maps directly to a team directory use case. Users can inline-edit any field, add new team members with one click, and remove leavers — all without navigating to individual edit forms.
CRM contact list and account data editing
Add columns for Company, Phone, Status, and Deal Stage. Wire the save button to your CRM API. The inline edit pattern lets sales teams update contact data quickly during a call or meeting without leaving the list view.
Product catalogue and inventory management tables
Use for product name, SKU, price, and stock columns. The inline edit is faster than opening individual product edit pages for bulk updates. Add input type="number" for price and stock columns and a min attribute for stock (min="0").
Spreadsheet-style data entry and bulk import
Add a CSV paste handler to the table: on paste into any input, parse the clipboard text as CSV rows and push each to the rows array. This lets users paste tabular data from Excel or Google Sheets directly into the editable table.
Learn the render-from-array pattern and inline edit technique
The table demonstrates the simplest stateful UI pattern in vanilla JavaScript: maintain an array of objects, re-render the table from the array on every change. The transparent input that shows a border on focus demonstrates the CSS-only inline edit affordance — the same technique used in the single-field inline edit field snippet.
Config tables, feature flags, and settings grids
Adapt for feature flag management: key, value, environment, enabled (checkbox column). Each row is a flag. The save button pushes all flag changes to your feature flag API in one batch, reducing the number of API calls compared to per-row saves.
Related: Expandable Row Detail Table
See the Expandable Row Detail Table for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each table cell contains an input element with a transparent border (border: 1.5px solid transparent) and transparent background. On hover, .cell-input:hover adds border-color: #e2e8f0 — a faint visible border. On focus (click to edit), .cell-input:focus adds border-color: #6366f1 and a box-shadow ring. This creates the appearance of plain text that becomes an editable field when clicked — the inline edit pattern. The input type and value are set from the row data array on each render().

In the render forEach loop, check the column key: if (key === "status") { const sel = document.createElement("select"); sel.className = "cell-input"; ["Active","Inactive","Pending"].forEach(opt => { const o = document.createElement("option"); o.value = o.textContent = opt; if (r[key] === opt) o.selected = true; sel.appendChild(o); }); sel.onchange = e => rows[i][key] = e.target.value; td.appendChild(sel); } else { /* existing input code */ }. Style the select the same as .cell-input.

In saveData(), validate each row: const errors = rows.flatMap((r, i) => { const rowErrors = []; if (!r.name.trim()) rowErrors.push("Row "+(i+1)+": Name is required"); if (r.email && !r.email.includes("@")) rowErrors.push("Row "+(i+1)+": Invalid email"); return rowErrors; }); if (errors.length) { alert(errors.join("\n")); return; }. For inline validation, add .cell-input.invalid { border-color: #ef4444; } and set the class when the input blurs with an invalid value.

Click "JSX" to download. Manage rows with useState(initialRows). For controlled inputs, bind value={row.key} and onChange={e => setRows(prev => prev.map((r,i) => i===idx ? {...r, [key]: e.target.value} : r))}. For add row: setRows(prev => [...prev, {name:"",role:"",dept:"",email:""}]). For delete: setRows(prev => prev.filter((_,i) => i !== idx)). Focus the new row input using a useEffect with a dependency on rows.length.