Skip to content

DirectorySkills

Spreadsheet grid: 1440, made in this run
Run 1Spreadsheet grid“We need an editable grid for staff hours this week, like a spreadsheet. Details in brief.md.”
Runs in
  • Claude Code
  • Codex
  • Cursor
  • Gemini CLI
  • GitHub Copilot
  • OpenCode
Generates with
Your agent's model
Tested with
Claude Code 2.1.322Claude Sonnet 5.51 real run, 10 Nov
Needs
Node 20+Chrome
Licence
Recaply licence

Spreadsheet Grid

An editable spreadsheet-style grid: lettered columns, numbered rows, a formula bar, keyboard moves and live totals.

$9.99once, yours to keep

Builds an editable spreadsheet-style data grid into your own project: a sheet with letters across the top and row numbers down the side; a formula bar shows the chosen cell's name and value or formula; arrow keys move the green selection, Enter or typing edits, Escape cancels; "Left" and the totals row work themselves out, overspends show in red with a minus sign. Plain HTML, CSS and JavaScript, or matched to React, Next.js, Vue, Svelte, Astro or Tailwind when the project uses them. Comes with a working reference and a check that screenshots it at phone, tablet and desktop widths and flags sideways overflow, text under 12px, missing alt text and names, low contrast, no focus styles, motion that ignores reduced motion, leftover sample words and emoji icons. Use when a product needs people to edit numbers in a familiar sheet (budgets, quotes, rotas, stock counts), or someone asks for a spreadsheet, editable grid or Excel-like table.

By RecaplyUpdated 12 Nov

Install Spreadsheet Grid

After the download, ask your agent to add it, or run the line yourself.

Read https://recaply.co/skills/spreadsheet-grid/install.md and follow it to install the spreadsheet-grid skill from spreadsheet-grid.zip.

Paste it into your agent. It reads our install steps, finds the ZIP you downloaded and adds the skill for you.

New to skills? How to install a skill

Similar skills

All 735 skills

Who Spreadsheet Grid is for

What you get

  • One HTML file with its CSS inside: no build step, no framework
  • An editable spreadsheet-style data grid
  • Checked at phone and desktop widths

Good fit

  • When a product needs people to edit numbers in a familiar sheet, or someone asks for a spreadsheet, editable grid or Excel-like table

Not for

  • An app with a backend, sign-in or a database
  • Copying another company's site

About this skill

You say

We need an editable grid for staff hours this week, like a spreadsheet. Details in brief.md.

It said back

The staff hours grid is built as plain HTML, CSS and JavaScript, since the folder had no framework. The automated check passes at 390, 768 and 1440px: no sideways overflow, no text under 12px, and contrast, focus styles and reduced-motion handling are all fine. I looked at the 1440px screenshot and it reads cleanly.

I did not test the over-40 flag by typing hours in, so that part is untested. I also haven't opened the 390px and 768px screenshots myself.

Show more

What it did in run 1

  1. ./brief.md
  2. spreadsheet-grid
  3. *
  4. ./.claude/skills/spreadsheet-grid/reference/index.html
  5. ./.claude/skills/spreadsheet-grid/reference/script.js
  6. ./.claude/skills/spreadsheet-grid/reference/style.css
  7. cat > index.html <<'EOF' <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Bramble Café rota – hours this week</title> <meta name="description" content="Editable weekly hours for Bramble Café staff, with totals, pay and a flag for anyone over 40 hours."> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap"> <link rel="stylesheet" href="style.css"> </head> <body> <a class="skip" href="#main">Skip to content</a> <main id="main"> <section class="d-sec" aria-labelledby="rota-h"> <div class="sec-head"><h2 id="rota-h">Bramble Café <em>rota</em></h2><p class="sub">Hours worked this week. Type hours for each day; totals and pay work themselves out, and anyone over 40 hours is flagged.</p></div> <div class="wrap"><div class="ss-box"> <div class="fx"><span class="fx-ref" data-ref>A2</span><span class="fx-val" data-fx></span></div> <div class="ss-frame"><table class="ss" role="grid" aria-label="Staff hours this week" aria-describedby="ss-help"></table></div> <p class="d-hint" id="ss-help">Arrow keys move. Enter or F2 edits a cell, Escape cancels, Enter saves. Total hours, pay (£12.21 an hour) and the bottom row work themselves out and can't be edited. Anyone over 40 hours is marked "Over 40" in red.</p> <p class="sr-only" role="status" data-live></p> </div></div> </section> </main> <script src="script.js" defer></script> </body> </html> EOF cat > style.css <<'EOF' :root{--bg:#F6F0E0;--surface:#FFFDF6;--ink:#17261D;--muted:#4D5A50;--line:#D6CFB8;--accent:#1F4D36;--on-accent:#F6F0E0;--em:#1F4D36;--bad:#B42318;--head:#EDE5CE;--sel:#D3E3D2} *,*::before,*::after{box-sizing:border-box} html{-webkit-text-size-adjust:100%} body{margin:0;background:var(--bg);color:var(--ink);font-family:'IBM Plex Sans',system-ui,sans-serif;font-size:1.0625rem;line-height:1.6} h2{font-family:'IBM Plex Sans',system-ui,sans-serif;line-height:1.1;margin:0;font-weight:700} p{margin:0} :focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:4px} .wrap{width:min(100% - 40px,1180px);margin-inline:auto} .skip{position:absolute;left:-9999px;top:12px;z-index:100;background:var(--accent);color:var(--on-accent);padding:.6rem 1rem;border-radius:8px} .skip:focus{left:12px} .sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} .d-sec{padding-block:clamp(40px,8vw,96px)} .sec-head{width:min(100% - 40px,1180px);margin:0 auto clamp(24px,3.4vw,40px)} .sec-head h2{font-size:clamp(1.8rem,3.8vw,2.8rem);letter-spacing:-.02em;color:var(--accent)} .sec-head h2 em{font-style:italic} .sec-head .sub{color:var(--muted);margin-top:.6rem;max-width:60ch;font-size:1.05rem} .d-hint{font-size:.95rem;color:var(--muted);padding:.9rem .8rem;max-width:80ch} .ss-box{background:var(--surface);border:1px solid var(--line);border-radius:10px;overflow:hidden} .fx{display:flex;align-items:center;gap:.8rem;padding:.55rem .8rem;border-bottom:1px solid var(--line);font-family:'IBM Plex Mono',monospace;font-size:.95rem;min-height:46px} .fx-ref{min-width:3.4rem;padding:.2rem .5rem;border:1px solid var(--line);border-radius:6px;text-align:center;font-weight:500} .fx-val{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .fx-val::before{content:"fx";font-style:italic;color:var(--muted);margin-right:.7rem} .ss-frame{overflow-x:auto} .ss{border-collapse:collapse;min-width:980px;width:100%;font-size:.95rem} .ss th{background:var(--head);color:var(--muted);font-weight:600;font-size:.9rem;border:1px solid var(--line);height:32px;padding:0 .5rem} .ss thead th{text-align:center} .ss tbody th{width:44px;text-align:center;position:sticky;left:0;z-index:1} .ss .ss-corner{width:44px;position:sticky;left:0;z-index:2} .ss th.on{background:var(--sel);color:var(--em)} .ss td{border:1px solid var(--line);height:36px;padding:0 .6rem;white-space:nowrap;cursor:cell;position:relative;outline:none} .ss td.n{text-align:right;font-family:'IBM Plex Mono',monospace;width:80px} .ss td.tc{width:160px}.ss td.pc{width:110px} .ss td.f{background:#F3EEDD} .ss td.neg{color:var(--bad);font-weight:600} .ss td .flag{font-family:'IBM Plex Sans',system-ui,sans-serif;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;margin-right:.6rem} .ss td.lab,.ss .tot td{font-weight:600} .ss .tot td{border-top:2px solid var(--accent)} .ss td[tabindex="0"]{box-shadow:inset 0 0 0 2px var(--accent)} .ss td[tabindex="0"]:focus-visible{box-shadow:inset 0 0 0 3px var(--accent)} .ss td input{position:absolute;inset:0;width:100%;height:100%;border:0;padding:0 .6rem;font:inherit;background:#fff;box-shadow:inset 0 0 0 2px var(--accent);outline:none} .ss td.n input{text-align:right} @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} EOF cat > script.js <<'EOF' // Bramble Café rota: roving selection, editing in place, formula bar, worked-out totals, over-40 flag. const RATE = 12.21, LIMIT = 40; const STAFF = ['Ana', 'Ben', 'Chloe', 'Dev', 'Ella', 'Femi']; const HEAD = ['Staff', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Total hours', 'Pay']; const L = 'ABCDEFGHIJ'.split(''), LAST = 9, TOT = 8, FIRST = 2, LASTSTAFF = FIRST + STAFF.length - 1, BOTTOM = LASTSTAFF + 1; const grid = document.querySelector('.ss'), ref = document.querySelector('[data-ref]'), fx = document.querySelector('[data-fx]'), live = document.querySelector('[data-live]'); const num = (n, d) => Number(n).toLocaleString('en-GB', { minimumFractionDigits: d, maximumFractionDigits: d }); const hrs = (v) => num(v, v % 1 ? 2 : 0), pay = (v) => '£' + num(v, 2); // Build the table let html = '<thead><tr><th class="ss-corner" aria-hidden="true"></th>' + L.map((l) => <th scope="col" data-col="${l}">${l}</th>).join('') + '</tr></thead><tbody>'; for (let r = 1; r <= BOTTOM; r++) { html += <tr${r === BOTTOM ? ' class="tot"' : ''}><th scope="row">${r}</th>; for (let c = 0; c <= LAST; c++) { const label = r === 1, calc = !label && (c >= TOT || r === BOTTOM) && c > 0; const cls = [label || r === BOTTOM && c === 0 ? 'lab' : '', c > 0 && !label ? 'n' : '', calc ? 'f' : '', c === TOT ? 'tc' : '', c === LAST ? 'pc' : ''].join(' ').trim(); const text = label ? HEAD[c] : c === 0 ? (r === BOTTOM ? 'Total' : STAFF[r - FIRST]) : ''; html += <td role="gridcell" ${cls ? class="${cls}" : ''}${calc ? 'aria-readonly="true" ' : ''}${label || r === BOTTOM && c === 0 ? 'aria-readonly="true" ' : ''}data-r="${r}" data-c="${c}" tabindex="${r === FIRST && c === 0 ? 0 : -1}">${text}</td>; } html += '</tr>'; } grid.innerHTML = html + '</tbody>'; const cell = (r, c) => grid.querySelector(td[data-r="${r}"][data-c="${c}"]); const val = (r, c) => Number(cell(r, c).dataset.v || 0); function setCalc(c, text, v, over) { c.dataset.v = v; c.classList.toggle('neg', !!over); c.textContent = ''; if (over) { const s = document.createElement('span'); s.className = 'flag'; s.textContent = 'Over ' + LIMIT; c.append(s); } c.append(text); } function recalc() { const col = Array(LAST + 1).fill(0); for (let r = FIRST; r <= LASTSTAFF; r++) { let t = 0; for (let c = 1; c <= 7; c++) { const v = val(r, c); t += v; col[c] += v; } setCalc(cell(r, TOT), hrs(t), t, t > LIMIT); setCalc(cell(r, LAST), pay(t * RATE), t * RATE); col[TOT] += t; } for (let c = 1; c <= 7; c++) setCalc(cell(BOTTOM, c), hrs(col[c]), col[c]); setCalc(cell(BOTTOM, TOT), hrs(col[TOT]), col[TOT]); setCalc(cell(BOTTOM, LAST), pay(col[TOT] * RATE), col[TOT] * RATE); } recalc(); let cur = cell(FIRST, 0), editing = null; function formula(c) { const r = +c.dataset.r, k = +c.dataset.c; if (r === BOTTOM && k > 0) return k === LAST ? =I${BOTTOM}*${RATE} : =SUM(${L[k]}${FIRST}:${L[k]}${LASTSTAFF}); if (r >= FIRST && r <= LASTSTAFF) { if (k === TOT) return =SUM(B${r}:H${r}); if (k === LAST) return =I${r}*${RATE}; } return k > 0 && r > 1 ? (c.dataset.v ? String(c.dataset.v) : '') : c.textContent; } function describe(c) { let s = L[+c.dataset.c] + c.dataset.r + ': ' + (c.textContent.replace(/^Over \d+/, '').trim() || 'empty'); if (c.classList.contains('neg')) s += ', over ' + LIMIT + ' hours'; return s + (c.getAttribute('aria-readonly') === 'true' ? ', read only' : ''); } function select(c, focus) { if (!c) return; cur.tabIndex = -1; cur = c; c.tabIndex = 0; if (focus !== false) c.focus(); ref.textContent = L[+c.dataset.c] + c.dataset.r; fx.textContent = formula(c); grid.querySelectorAll('th.on').forEach((h) => h.classList.remove('on')); grid.querySelector(thead th[data-col="${L[+c.dataset.c]}"]).classList.add('on'); c.parentElement.querySelector('th').classList.add('on'); if (focus !== false) live.textContent = describe(c); } const editable = (c) => c.getAttribute('aria-readonly') !== 'true'; function edit(c, start) { if (!editable(c) || editing) return; const isN = c.classList.contains('n'), old = isN ? (c.dataset.v || '') : c.textContent; const inp = document.createElement('input'); inp.value = start !== undefined ? start : old; inp.setAttribute('aria-label', 'Edit ' + L[+c.dataset.c] + c.dataset.r); if (isN) inp.inputMode = 'decimal'; c.textContent = ''; c.appendChild(inp); inp.focus(); if (start === undefined) inp.select(); editing = { c, inp, old, isN }; } function done(save, move, focus) { if (!editing) return; const e = editing; editing = null; const v = save ? e.inp.value.trim() : String(e.old); if (e.isN) { let n = v === '' ? 0 : Number(v.replace(/,/g, '')); const bad = !isFinite(n) || n < 0 || n > 24; if (bad) { n = Number(e.old || 0); if (save) live.textContent = 'Hours must be a number from 0 to 24'; } e.c.dataset.v = n || ''; if (!n) delete e.c.dataset.v; e.c.textContent = n ? hrs(n) : ''; recalc(); } else e.c.textContent = v; const r = +e.c.dataset.r, k = +e.c.dataset.c; select(move === 'down' ? cell(Math.min(BOTTOM, r + 1), k) : move === 'right' ? cell(r, Math.min(LAST, k + 1)) : e.c, focus); } grid.addEventListener('click', (e) => { const c = e.target.closest('td'); if (c && !editing) select(c); }); grid.addEventListener('dblclick', (e) => { const c = e.target.closest('td'); if (c) { select(c); edit(c); } }); grid.addEventListener('keydown', (e) => { if (editing) { if (e.key === 'Enter') { e.preventDefault(); done(true, 'down'); } else if (e.key === 'Tab') { e.preventDefault(); done(true, 'right'); } else if (e.key === 'Escape') { e.preventDefault(); done(false); } return; } const r = +cur.dataset.r, k = +cur.dataset.c; const to = { ArrowUp: [r - 1, k], ArrowDown: [r + 1, k], ArrowLeft: [r, k - 1], ArrowRight: [r, k + 1], Home: [r, 0], End: [r, LAST] }[e.key]; if (to) { e.preventDefault(); select(cell(Math.max(1, Math.min(BOTTOM, to[0])), Math.max(0, Math.min(LAST, to[1])))); return; } if (e.key === 'Enter' || e.key === 'F2') { e.preventDefault(); edit(cur); return; } if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); edit(cur, e.key); } }); grid.addEventListener('focusout', (e) => { if (editing && e.target === editing.inp) done(true, undefined, false); }); select(cur, false); EOF node .claude/skills/spreadsheet-grid/scripts/check.mjs index.html --out check 2>&1 | tail -30
  8. ls; timeout 120 node .claude/skills/spreadsheet-grid/scripts/check.mjs index.html --out check 2>&1 | tail -30
  9. ./index.html
  10. ./style.css
  11. ./script.js
  12. timeout 180 node .claude/skills/spreadsheet-grid/scripts/check.mjs index.html --out check 2>&1 | tail -30
  13. ./check/1440.png
  14. Done

10 Nov 2026 · Claude Code 2.1.322 with Claude Sonnet 5.5 · 1 min 43 s · $0.32 of API use

Questions

Is Spreadsheet Grid free?

No. Spreadsheet Grid costs $9.99 once and stays in your Recaply account, or it comes with the Recaply plan ($19.99 a month), which opens every premium skill and prompt.

Which agents can use Spreadsheet Grid?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

What do I get when I buy Spreadsheet Grid?

A ZIP of the whole skill folder (34 KB): the SKILL.md your agent follows and its 6 other files, the references, templates and scripts it uses. You download it from this page whenever you need it, and install it in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. The Recaply plan opens it too, with every other premium skill and prompt.

How do I install Spreadsheet Grid?

Buy it, then download the ZIP from this page. For Claude Code, unzip it into .claude/skills/ in your project. For Codex, Cursor and Gemini CLI, unzip it and run npx skills add ./spreadsheet-grid with your agent's flag.

What do I get from Spreadsheet Grid?

One HTML file with its CSS inside: no build step, no framework. An editable spreadsheet-style data grid. Checked at phone and desktop widths.

What does Spreadsheet Grid need?

Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.

How big is Spreadsheet Grid?

7 files, about 22,957 tokens in all. The SKILL.md is about 2,206 tokens, and your agent reads it only when the job fits.

What does a run cost?

Our 1 runs took 1 min 43 s to 1 min 43 s and $0.32 to $0.32 of API use each, in Claude Code with claude-sonnet-5-5. On a Claude plan, a run counts toward your usage instead.

Do I have to name Spreadsheet Grid to use it?

Type /spreadsheet-grid first to make sure it loads. Without it, Claude Code may judge a small job simple enough to do without the skill.