View, sort and edit a CSV, with an undo stack that goes back exactly one step and a parse report that tells a broken quote from a ragged row
Initializing in your browser…
Generate SQL that runs: identifiers quoted rather than stripped, values escaped per dialect, and every change to your data reported
Convert both ways, converting a type only when it reads back the same, and naming every value that cannot
Convert CSV to XLSX or ODS with cell types that keep leading zeros, long IDs and dates intact
Four rows sorted two ways, then five edits and five undos. The sort is the part that did not exist before, and the undo is the part that worked once and then went wrong.
A small file
name,n delta,10 alpha,2 Charlie,1 bravo,
Two orderings, and the undo stack
By name, ascending, text: Charlie · alpha · bravo · delta Unicode code point order, so the capital sorts first and every reader gets the same answer. 4 values. By n, ascending, number: 1 · 2 · 10 · (blank) 3 values, 1 blank. Blanks stay at the chosen end. Five edits, then five undos: each lands exactly one step back.
Two things here are new and one is a fix. There was no sort at all before, which on a grid paginated at a hundred rows a page is a real gap; text now sorts by code point rather than by whatever collation the reader's browser has, so the same file sorts the same way for everyone, and the page says which comparison it used. Editing a sorted view writes to that row of the FILE rather than to the position on screen. And undo used to keep its stack in one piece of state and its pointer in another: over four edits and three undos, the first undo was right, the second did nothing at all, and the third went back one step too far. The document and its history are one object now.
Open a CSV in a spreadsheet-style grid, sort it by any column, edit cells, add and remove rows and columns, and download the result. Every edit goes on an undo stack that goes back exactly one step at a time, the ordering says what it is comparing, and the difference between a file that cannot be read and one that is merely ragged is shown on the page rather than in a toast that disappears.
CSV is a deceptively simple format. RFC 4180 fits on two pages, and real files violate every rule in it: semicolon-separated exports, tab-separated files with a .csv extension, mixed quoting in one file, ragged row lengths, commas in unquoted fields. The producing tools were permissive, so the parser has to be. This viewer parses with PapaParse and shows the result in a grid, paginated at 100 rows so a large file stays responsive.
Measured on 2026-09-02 by making edits and reading the download back with Python's csv module. **Undo worked once and then went wrong.** The stack lived in one piece of state and the pointer in another, and the pointer was updated from a stale closure. Over four edits and three undos: the first undo was right, the second did nothing at all, the third went back one step further than it should, and redo landed in the wrong place. The document and its history are one object now, past, present and future together, so the stack and the pointer cannot disagree. Five edits, five undos and five redos each land exactly one step away.
**Pressing Enter in a cell editor saved the value and left the editor open.** Saving re-renders, which re-registers the window key listener DURING the same event dispatch, and the new listener saw no cell being edited and reopened the editor on the same keystroke. The editor stops the event now.
**Paste wrote into the row array in place.** `const newData = [...csvData]` copies the outer array and not the rows, so writing to `newData[r][c]` changed the current data before it was put on the undo stack. Every mutation goes through one function that never writes into an existing row.
**A single-column CSV was refused outright**, because papaparse reports UndetectableDelimiter for a file with no delimiter in it to detect and every Delimiter-type error was treated as fatal. Its own documentation classes that as a warning: the parse has already defaulted to a comma and is correct.
**An empty cell and a cell holding a dash rendered identically**, both as a dash, which is two different facts about a file shown as one.
There was no sort at all, and a hundred rows a page with no way to order them is a gap. There is one now, built on the shared ordering module: a key per cell computed once and compared, which is transitive by construction, with six orderings (automatic, text, number, natural, date, length), blanks kept at a chosen end, and text compared by Unicode code point rather than by the reader's browser locale, so the same file sorts the same way for everyone. The page states the comparison it used and counts the values it could and could not read. Editing a sorted view writes to the right row of the FILE, not to the position on screen, and the download comes out in the order shown.
The export goes through the shared RFC 4180 writer: CRLF records per section 2.1, quoting as a minimum rather than a preference, and a UTF-8 byte order mark so a spreadsheet reads accented and non-Latin text correctly. Read back by a separate CSV implementation, a cell holding a comma, a quote, a newline, surrounding spaces or nothing at all all survive the round trip.
Open the file, find the wrong values with search, correct them, and download. If you go too far, undo steps back one edit at a time.
The page says whether the parse failed structurally, usually an unclosed quote, or whether the rows simply have different widths, which is not a parse fault at all.
Sort by any column with the ordering stated. Numbers sort by value, dates by the calendar, and text by code point rather than by whatever collation the reader's browser happens to have.
A file with one column per line loads, where it used to be refused because there was no delimiter in it to detect.
Yes, and that is now tested: five edits, five undos and five redos each land exactly one step away. The previous version kept the stack and the pointer in separate pieces of state and updated one of them from a stale value, so the first undo was right and the ones after it were not.
It changes the order the rows are shown and downloaded in, and nothing else. An edit made while a sort is in force is written to that row of the file, not to the position it happens to occupy on screen.
The panel at the top says which of two things happened. A structural fault, usually an unclosed quote, means the fields after it cannot be told apart and nothing is loaded. A ragged file, where rows have different numbers of fields, loads fine and the panel says how many widths there are, because RFC 4180 asks for one and real files often have several.
No. Everything happens on a copy in your browser and the file on your disk is never written to. The download is a new file.
It is a UTF-8 byte order mark, three bytes that tell a spreadsheet the file is UTF-8. Without one, Excel guesses, and it usually guesses wrong for accented or non-Latin text. Every CSV reader worth using strips it.
By Unicode code point unless you choose a language collation. Code point order is the same for every reader; a locale order is not, so the same file sorted by name comes out differently for a Swedish reader, where the ring-A sorts after Z. The page states which comparison it used.
Rows and columns are parsed and transformed in memory in your browser. No record ever reaches a server.