Editable Table — Free HTML CSS JS Inline Edit Snippet

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than tracing the re-render approach yourself, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why render() clears and rebuilds the entire tbody's innerHTML from the rows array on every single keystroke's oninput handler, rather than mutating only the one changed cell, and what that costs versus a more surgical DOM update. The same assistant can help optimize it, for instance checking whether rebuilding all rows on every keystroke causes focus loss or cursor-jump bugs as the table grows larger. It's also useful for extending the table: ask it to add per-cell validation styling for invalid emails, support pasting a block of tab-separated spreadsheet data across multiple cells at once, or add column sorting that reorders the underlying rows array. 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 an inline-editable data table in plain HTML, CSS, and JavaScript with no library.

Requirements:
- Keep all table data in a single array of plain objects (not scattered across DOM element properties), and make one render function the only place that builds the table body, iterating that array to create a row per object and a real text input inside each cell (not a contenteditable div) pre-filled with that field's current value.
- Wire each cell's input so that typing in it writes the new value back into the corresponding object in the underlying array immediately (not only on blur or Enter), keeping the array as the single source of truth for what the table currently contains.
- Style the inputs so they are visually indistinguishable from plain table text at rest (transparent border and background), gain a subtle border on hover to hint they're editable, and show a clear focused border and highlight ring when actively being edited.
- Implement an "add row" button that appends a new blank object to the array, re-renders the table, then automatically focuses the first input of the newly added row and applies a brief slide-in highlight animation to just that row so the user notices where the new row landed.
- Implement a delete button on each row (only visible or emphasized on row hover) that removes that specific object from the array by its position and re-renders, plus a live row-count readout that updates automatically after every add or delete.
- Implement a "save" action that gathers the full current array, filters out entirely empty rows, and is structured so swapping in a real fetch POST to a backend endpoint would be a one-line change.

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.

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.