Source Code

<div class="skn-card">
  <div class="skn-head">
    <h3>Sheet1</h3>
    <p class="skn-hint">Arrow keys to move &middot; Tab / Shift+Tab &middot; Enter to edit &middot; Esc to cancel</p>
  </div>
  <table class="skn-table" id="sknTable" tabindex="0"></table>
</div>

Spreadsheet Keyboard Navigation Table — Excel-Style Arrow Key Grid HTML CSS JS

Spreadsheet Keyboard Navigation Table · Tables · Plain HTML, CSS & JS · Live preview

What's included

Features

Tracked active-cell state
A single {row, col} object is the source of truth, not scattered DOM focus state.
Real arrow-key navigation
ArrowUp/Down/Left/Right move the active cell with preventDefault, not decorative CSS hover.
Tab / Shift+Tab traversal
Tabs across columns like moving between spreadsheet fields, wrapping the standard convention.
Enter-to-edit, Escape-to-cancel
Enter swaps in a real input; Escape discards changes and restores the prior value.
Type-to-overwrite
Typing a printable character on an active cell starts editing with the field cleared, spreadsheet-style.
Commit-and-move on Enter/Tab
Saving a value automatically advances the active cell down or across.
Bounds-clamped movement
Every navigation call is clamped to the grid's dimensions, so edges never throw or wrap unexpectedly.
Click or dblclick entry
Mouse users can click to select a cell or double-click to jump straight into edit mode.

About this UI Snippet

Spreadsheet Keyboard Navigation Table — Real Arrow-Key Cell Tracking

Screenshot of the Spreadsheet Keyboard Navigation Table snippet rendered live

Data-entry-heavy tools live or die on keyboard efficiency — a user filling in dozens of cells shouldn't have to reach for the mouse between every one. This snippet rebuilds the core spreadsheet keyboard model in plain HTML, CSS, and vanilla JavaScript: a single "active cell" tracked in state, moved with arrow keys and Tab, entered with Enter, and committed or cancelled with Enter or Escape — the same muscle memory as Excel or Google Sheets.

One active cell, tracked in state, not in the DOM

The active position lives as a plain { row, col } object, not as a CSS :focus state on scattered inputs. setActive() clamps the requested row/col into the grid's bounds with Math.max/Math.min, removes the .active class from wherever it was, and applies it to the new cell — a single source of truth that every keyboard handler reads and writes, so moving with an arrow key is just changing two numbers and re-rendering the highlight.

Arrow keys move the active cell, not the page

The table's keydown listener (attached to a tabindex="0" table so it can receive focus and key events) intercepts ArrowUp/ArrowDown/ArrowLeft/ArrowRight with preventDefault() and calls setActive with the adjusted row or column — genuine navigation logic, not a static grid with a CSS :hover that merely looks interactive. Tab and Shift+Tab do the same along columns, wrapping the spreadsheet convention of "next field" onto a grid instead of a form.

Enter to edit, Escape to cancel, Tab to commit-and-move

Pressing Enter (or typing any printable character) on the active cell calls startEdit(), which swaps the cell's static <span> for a real <input> pre-filled with the current value, focused and selected. From there, Enter commits the new value back into the DATA array and moves the active cell down a row; Escape discards the edit and restores the original value; Tab commits and moves right (or left with Shift) — three distinct outcomes mapped to the three keys spreadsheets use for exactly this.

Typing to overwrite, the way spreadsheets behave

Beyond Enter, typing any single printable character while a cell is active immediately starts editing and clears the input's value before the keystroke lands — mirroring how clicking a spreadsheet cell and typing replaces its contents rather than requiring an explicit "clear first" step. This is handled in the default branch of the keydown switch, checking e.key.length === 1 to distinguish printable characters from control keys.

Bounds-checked movement

Every navigation call routes through the same clamp in setActive, so pressing an arrow key at the grid's edge simply has no effect rather than throwing, wrapping around unexpectedly, or selecting a cell outside the data array — a small detail that keeps the interaction predictable at every boundary.

Customizing it

Add column-based value formatting, wire real cell-range selection with Shift+Arrow, or add copy/paste with the clipboard API. Pair it with an editable table for a simpler single-row-at-a-time alternative, or a resizable columns table for adjustable widths.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than working through the state machine on your own, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the single tracked { row, col } active-cell object, rather than relying on DOM :focus, lets arrow keys, Tab, and mouse clicks all converge on the same setActive function without duplicating navigation logic three times. The same assistant can help you extend it — ask it to add Shift+Arrow range selection, Ctrl+C/Ctrl+V clipboard support for pasting tab-separated data across multiple cells at once (the way pasting from a real spreadsheet works), or per-column input types and validation (numbers only, dropdowns) that still fit the same edit/commit/cancel flow. 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 a "spreadsheet keyboard navigation" data grid in plain HTML, CSS, and JavaScript with no library — a table where an active cell can be moved entirely via the keyboard the way Excel or Google Sheets works.

Requirements:
- Track the currently active cell as a single { row, col } object in JavaScript state (not derived from DOM :focus), with a setActive(row, col) function that clamps the requested position into the grid's actual bounds using Math.max/Math.min before applying it, removes the active highlight class from the previous cell, and adds it to the new one.
- Attach one keydown listener to the table container (given tabindex="0" so it can receive focus and keyboard events) that handles ArrowUp/ArrowDown/ArrowLeft/ArrowRight by calling preventDefault and moving the active cell one row or column in that direction via setActive, so the browser's own scroll-on-arrow-key behavior never fires instead.
- Handle Tab and Shift+Tab in that same keydown listener to move the active cell one column right or left respectively, calling preventDefault so the browser's default focus-traversal tabbing does not also happen.
- Implement Enter (and, separately, typing any single printable character) as triggering "start edit" on the active cell: replace its static content with a real text input pre-filled with the cell's current value, focused and with its text selected.
- While editing, handle Enter on the input to commit the typed value back into the underlying data array and then move the active cell down one row; handle Escape to discard the edit and restore the cell's previous value without writing anything back; handle Tab/Shift+Tab to commit the value and move the active cell right or left.
- Support mouse interaction too: a single click on any cell sets it as the active cell (without entering edit mode), and a double-click sets it active and immediately starts editing.
- Render the grid from a plain 2D array of row data plus a column-labels array, building the table's HTML in a loop rather than hand-writing every cell.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn order-line grid renders with the first cell active (highlighted with an indigo outline).
  2. 2
    Press arrow keysThe active cell moves up, down, left, or right — clamped so it never leaves the grid.
  3. 3
    Press Tab / Shift+TabThe active cell moves right or left across the row, like tabbing between spreadsheet fields.
  4. 4
    Press Enter to editThe active cell becomes a real input, pre-filled and selected, ready to overwrite.
  5. 5
    Press Enter or Tab to commitEnter saves the value and moves down a row; Tab saves and moves across a column.
  6. 6
    Press Escape to cancelThe edit is discarded and the cell reverts to its previous value.

Real-world uses

Common Use Cases

Bulk data entry grids
Fast keyboard-only entry for order lines, inventory counts, or line-item forms.
Internal admin tools
Pair with an editable table for a hybrid of full-grid and per-row editing.
Financial and budgeting sheets
Navigate and edit a grid of figures the way finance teams expect from a spreadsheet.
Import review and correction UIs
Let users quickly fix cells in an imported dataset without touching the mouse.
Custom spreadsheet components
A foundation for a lightweight in-house grid without pulling in a full spreadsheet library.
Learning keyboard-driven UI
A clear reference for tracked active-element state and keydown-driven navigation.
Related: Column Widths That Persist (localStorage)
See the Column Widths That Persist (localStorage) for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Track a second { row, col } anchor point set on the initial selection, and on Shift+Arrow extend the active cell without moving the anchor, then apply an active-range class to every cell between anchor and active (inclusive) in both dimensions instead of just one .active cell.

A single delegated listener on the tabindex="0" table catches every keypress regardless of which cell is logically active, avoiding the need to attach and remove listeners as the active cell changes. The handler reads the current active state and computes the new position, which is simpler and less error-prone than per-cell listeners.

In the commit(save) function inside startEdit, check input.value against your validation rule before writing it into DATA[active.row][active.col] — if invalid, keep editing is true and show an error state instead of calling commit, or revert to the previous value and flash an invalid style.

Add a keydown handler for Ctrl/Cmd+C that reads DATA[active.row][active.col] onto the clipboard via the Clipboard API, and Ctrl/Cmd+V that reads clipboard text, splits it on tabs and newlines for multi-cell paste, and writes the values into DATA starting at the active cell before re-rendering.

Keep DATA and the active { row, col } in component state (useState/useReducer, a reactive ref, or a component field). Bind the keydown handler to the grid container, compute the new active position the same way, and let the framework re-render the .active class and any editing input based on state — the navigation math is plain JavaScript and ports unchanged.