Open demo

CSS code

HTML
<div><span class="lbl">Edit button and double-click</span>
<section class="ed" data-edit-list aria-labelledby="nl-title">
  <h2 id="nl-title" tabindex="-1">Newsletter, issue 42</h2>
  <p class="ed-hint">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="ed-list" data-list aria-label="Newsletter tasks">
    <li class="ed-row"><input class="ed-box" type="checkbox" id="nl-1" aria-labelledby="nl-1-t"><span class="ed-text" id="nl-1-t" data-text>Draft the intro paragraph</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-ibtn" type="button" data-edit aria-label="Edit “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="ed-ibtn danger" type="button" data-delete aria-label="Delete “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="nl-2" aria-labelledby="nl-2-t" checked><span class="ed-text" id="nl-2-t" data-text>Pick three reader questions</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-ibtn" type="button" data-edit aria-label="Edit “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="ed-ibtn danger" type="button" data-delete aria-label="Delete “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="nl-3" aria-labelledby="nl-3-t"><span class="ed-text" id="nl-3-t" data-text>Find a header photo for the bakery story</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-ibtn" type="button" data-edit aria-label="Edit “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="ed-ibtn danger" type="button" data-delete aria-label="Delete “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="nl-4" aria-labelledby="nl-4-t"><span class="ed-text" id="nl-4-t" data-text>Check every link in the events list</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-ibtn" type="button" data-edit aria-label="Edit “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="ed-ibtn danger" type="button" data-delete aria-label="Delete “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="nl-5" aria-labelledby="nl-5-t"><span class="ed-text" id="nl-5-t" data-text>Schedule the send for Tuesday at 08:00</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-ibtn" type="button" data-edit aria-label="Edit “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="ed-ibtn danger" type="button" data-delete aria-label="Delete “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
  </ul>
  <div class="ed-toast" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p data-toast-msg></p><button class="ed-undo" type="button" data-undo>Undo</button><button class="ed-tx" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="sr-only" role="status" data-live></p>
</section></div>

<div><span class="lbl">Compact list with text buttons</span>
<section class="ed compact" data-edit-list aria-labelledby="bug-title">
  <h2 id="bug-title" tabindex="-1">Bug bash follow-ups</h2>
  <p class="ed-hint">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="ed-list" data-list aria-label="Bug bash tasks">
    <li class="ed-row"><input class="ed-box" type="checkbox" id="bug-1" aria-labelledby="bug-1-t"><span class="ed-text" id="bug-1-t" data-text>Login button jumps on Safari 17</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-tbtn" type="button" data-edit aria-label="Edit “Login button jumps on Safari 17”">Edit</button><button class="ed-tbtn danger" type="button" data-delete aria-label="Delete “Login button jumps on Safari 17”">Delete</button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="bug-2" aria-labelledby="bug-2-t"><span class="ed-text" id="bug-2-t" data-text>Export ignores the date filter</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-tbtn" type="button" data-edit aria-label="Edit “Export ignores the date filter”">Edit</button><button class="ed-tbtn danger" type="button" data-delete aria-label="Delete “Export ignores the date filter”">Delete</button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="bug-3" aria-labelledby="bug-3-t" checked><span class="ed-text" id="bug-3-t" data-text>Typo on the pricing page</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-tbtn" type="button" data-edit aria-label="Edit “Typo on the pricing page”">Edit</button><button class="ed-tbtn danger" type="button" data-delete aria-label="Delete “Typo on the pricing page”">Delete</button></span></li>
    <li class="ed-row"><input class="ed-box" type="checkbox" id="bug-4" aria-labelledby="bug-4-t"><span class="ed-text" id="bug-4-t" data-text>Toast covers the cookie banner on phones</span><input class="ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="ed-tbtn" type="button" data-edit aria-label="Edit “Toast covers the cookie banner on phones”">Edit</button><button class="ed-tbtn danger" type="button" data-delete aria-label="Delete “Toast covers the cookie banner on phones”">Delete</button></span></li>
  </ul>
  <div class="ed-toast" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p data-toast-msg></p><button class="ed-undo" type="button" data-undo>Undo</button><button class="ed-tx" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="sr-only" role="status" data-live></p>
</section></div>
CSS
/* Inline editing and undo: each row is a checkbox named by its text (aria-labelledby), a hidden text field the script
   shows for editing, and Edit / Delete buttons named after the task. Deleting shows a message with an Undo button. */
.ed{--ed-ink:#0b2730;--ed-muted:#46606a;--ed-line:#d6e6ea;--ed-box:#6a8790;--ed-accent:#0e7490;--ed-accent-dark:#155e75;--ed-hover:#eef7f9;
  --ed-danger:#b42318;--ed-danger-bg:#fdecea;--ed-toast:#0b2730;--ed-toast-ink:#e6f4f7;--ed-undo:#a5f3fc;
  position:relative;background:#fff;color:var(--ed-ink);border:1px solid var(--ed-line);border-radius:18px;padding:24px 24px 20px;box-shadow:0 18px 40px -28px rgba(11,39,48,.4)}
.ed [hidden]{display:none!important}
.ed :where(h2,p,ul){margin:0;padding:0}
.ed ul{list-style:none}
.ed h2{font-size:1.2rem;font-weight:800;letter-spacing:-.015em}
.ed h2:focus{outline:none}
.ed-hint{margin-top:6px!important;font-size:.85rem;line-height:1.5;color:var(--ed-muted)}
.ed-hint kbd{font:inherit;font-size:.78rem;font-weight:700;padding:1px 6px;border:1px solid #b7cdd3;border-bottom-width:2px;border-radius:6px;background:#f6fafb;color:var(--ed-ink)}
.ed-list{margin-top:14px!important;border-top:1px solid var(--ed-line)}
.ed-row{display:flex;align-items:center;gap:12px;min-height:56px;padding:8px 6px 8px 10px;border-bottom:1px solid var(--ed-line);transition:background-color .15s}
.ed-row:hover{background:var(--ed-hover)}
.ed-box{appearance:none;flex:none;width:20px;height:20px;margin:0;border:2px solid var(--ed-box);border-radius:6px;background:#fff center/13px no-repeat;cursor:pointer;
  transition:background-color .15s,border-color .15s}
.ed-box:checked{background-color:var(--ed-accent);border-color:var(--ed-accent);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E")}
.ed-box:focus-visible{outline:3px solid var(--ed-accent);outline-offset:2px}
.ed-text{flex:1;min-width:0;font-size:1rem;line-height:1.4;overflow-wrap:anywhere;cursor:text;user-select:none}
.ed-box:checked+.ed-text{color:var(--ed-muted);text-decoration:line-through}
.ed-field{flex:1;min-width:0;font:inherit;font-size:1rem;line-height:1.4;padding:6px 10px;margin:-1px 0;border:2px solid var(--ed-accent);border-radius:9px;background:#fff;color:inherit}
.ed-field:focus{outline:3px solid #a5e3ee;outline-offset:0}
.ed-acts{display:flex;gap:2px;flex:none}
.ed-ibtn{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:transparent;color:var(--ed-muted);cursor:pointer;transition:background-color .15s,color .15s}
.ed-ibtn:hover{background:#dff0f4;color:var(--ed-accent-dark)}
.ed-ibtn.danger:hover{background:var(--ed-danger-bg);color:var(--ed-danger)}
.ed button:focus-visible{outline:3px solid var(--ed-accent);outline-offset:2px}
/* undo message */
.ed-toast{display:flex;align-items:center;gap:10px;margin-top:14px;padding:10px 8px 10px 14px;border-radius:12px;background:var(--ed-toast);color:var(--ed-toast-ink);
  box-shadow:0 14px 30px -12px rgba(11,39,48,.55);animation:ed-in .22s ease-out}
.ed-toast>svg{flex:none;opacity:.8}
.ed-toast p{flex:1;min-width:0;font-size:.92rem;line-height:1.4;overflow-wrap:anywhere}
.ed-undo{flex:none;font:inherit;font-size:.9rem;font-weight:800;padding:7px 12px;border:0;border-radius:8px;background:transparent;color:var(--ed-undo);cursor:pointer;transition:background-color .15s}
.ed-undo:hover{background:rgba(165,243,252,.14)}
.ed-tx{flex:none;display:grid;place-items:center;width:32px;height:32px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--ed-toast-ink);cursor:pointer}
.ed-tx:hover{background:rgba(255,255,255,.1)}
.ed .ed-toast button:focus-visible{outline-color:var(--ed-undo)}
@keyframes ed-in{from{opacity:0;transform:translateY(6px)}}
/* compact variant */
.ed.compact{padding:20px 20px 16px;border-radius:14px}
.ed.compact h2{font-size:1.05rem}
.ed.compact .ed-row{min-height:46px;padding:5px 4px 5px 8px}
.ed.compact .ed-text{font-size:.95rem}
.ed-tbtn{font:inherit;font-size:.8rem;font-weight:700;padding:5px 9px;border:1px solid var(--ed-line);border-radius:8px;background:#fff;color:var(--ed-accent-dark);cursor:pointer;transition:background-color .15s,border-color .15s}
.ed-tbtn:hover{background:var(--ed-hover);border-color:#a9c8cf}
.ed-tbtn.danger{color:var(--ed-danger)}
.ed-tbtn.danger:hover{background:var(--ed-danger-bg);border-color:#f3b4ae}
.ed.compact .ed-acts{gap:6px}
.ed.compact .ed-field{font-size:.95rem;padding:4px 9px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:480px){.ed{padding:20px 16px 16px}.ed-row{padding-left:6px}}
@media (prefers-reduced-motion:reduce){.ed *{transition:none!important;animation:none!important}}
JavaScript
/* Inline editing and undo. Double-click a task or press Edit to rename it: Enter saves, Esc cancels, leaving the
   field saves. Delete removes the row and shows a message with Undo for 8 seconds, paused while it has the pointer or focus. */
document.querySelectorAll('[data-edit-list]').forEach((root) => {
  const list = root.querySelector('[data-list]');
  const live = root.querySelector('[data-live]');
  const toast = root.querySelector('[data-toast]');
  const msg = toast.querySelector('[data-toast-msg]');
  const q = (s) => '“' + s + '”';
  const say = (m) => { live.textContent = ''; setTimeout(() => { live.textContent = m; }, 60); };
  const name = (li) => li.querySelector('[data-text]').textContent;
  const bits = (li) => ({ text: li.querySelector('[data-text]'), field: li.querySelector('[data-field]'), actions: li.querySelector('[data-actions]') });
  let removed = null, timer = 0;

  const relabel = (li) => {
    li.querySelector('[data-edit]').setAttribute('aria-label', 'Edit ' + q(name(li)));
    li.querySelector('[data-delete]').setAttribute('aria-label', 'Delete ' + q(name(li)));
  };
  const startEdit = (li) => {
    if ('editing' in li.dataset) return;
    const { text, field, actions } = bits(li);
    li.dataset.editing = '';
    field.value = text.textContent;
    field.setAttribute('aria-label', 'Edit ' + q(text.textContent));
    text.hidden = true; actions.hidden = true; field.hidden = false;
    field.focus(); field.setSelectionRange(field.value.length, field.value.length);
  };
  const endEdit = (li, keep, refocus) => {
    if (!('editing' in li.dataset)) return;
    delete li.dataset.editing;
    const { text, field, actions } = bits(li);
    const value = field.value.trim().replace(/\s+/g, ' ');
    if (!keep) say('Edit cancelled.');
    else if (!value) say('A task needs a name. Kept ' + q(text.textContent) + '.');
    else if (value !== text.textContent) { text.textContent = value; relabel(li); say('Saved ' + q(value) + '.'); }
    field.hidden = true; text.hidden = false; actions.hidden = false;
    if (refocus) li.querySelector('[data-edit]').focus();
  };

  const hideToast = () => { clearTimeout(timer); toast.hidden = true; removed = null; };
  const arm = () => { clearTimeout(timer); timer = setTimeout(hideToast, 8000); };
  const pause = () => clearTimeout(timer);
  const resume = (e) => { if (!toast.hidden && !toast.contains(e.relatedTarget) && !toast.matches(':hover')) arm(); };
  toast.addEventListener('mouseenter', pause);
  toast.addEventListener('focusin', pause);
  toast.addEventListener('mouseleave', resume);
  toast.addEventListener('focusout', resume);

  list.addEventListener('click', (e) => {
    const li = e.target.closest('li');
    if (e.target.closest('[data-edit]')) { startEdit(li); return; }
    if (!e.target.closest('[data-delete]')) return;
    const next = li.nextElementSibling || li.previousElementSibling;
    removed = { li, index: [...list.children].indexOf(li) };
    li.remove();
    msg.textContent = 'Deleted ' + q(name(li));
    toast.hidden = false; arm();
    (next ? next.querySelector('input[type=checkbox]') : toast.querySelector('[data-undo]')).focus();
    say('Deleted ' + q(name(li)) + '. Undo is in the message below the list.');
  });
  list.addEventListener('dblclick', (e) => {
    const text = e.target.closest('[data-text]');
    if (text) startEdit(text.closest('li'));
  });
  list.addEventListener('keydown', (e) => {
    if (!e.target.matches('[data-field]')) return;
    if (e.key === 'Enter') { e.preventDefault(); endEdit(e.target.closest('li'), true, true); }
    if (e.key === 'Escape') { e.preventDefault(); endEdit(e.target.closest('li'), false, true); }
  });
  list.addEventListener('focusout', (e) => { if (e.target.matches('[data-field]')) endEdit(e.target.closest('li'), true, false); });
  list.addEventListener('change', (e) => {
    if (e.target.type === 'checkbox') say(q(name(e.target.closest('li'))) + (e.target.checked ? ' done.' : ' not done.'));
  });

  toast.querySelector('[data-undo]').addEventListener('click', () => {
    if (!removed) return;
    const { li, index } = removed;
    list.insertBefore(li, list.children[index] || null);
    hideToast();
    li.querySelector('input[type=checkbox]').focus();
    say('Restored ' + q(name(li)) + '.');
  });
  toast.querySelector('[data-toast-close]').addEventListener('click', () => {
    hideToast();
    (list.querySelector('input[type=checkbox]') || root.querySelector('h2')).focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Edit button and double-click</span>
<section class="card ed" data-edit-list aria-labelledby="nl-title">
  <h2 class="mb-0" id="nl-title" tabindex="-1">Newsletter, issue 42</h2>
  <p class="ed-hint">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="list-group list-group-flush ed-list" data-list aria-label="Newsletter tasks">
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="nl-1" aria-labelledby="nl-1-t"><span class="ed-text" id="nl-1-t" data-text>Draft the intro paragraph</span><input class="form-control ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn ed-ibtn" type="button" data-edit aria-label="Edit “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="btn ed-ibtn danger" type="button" data-delete aria-label="Delete “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="nl-2" aria-labelledby="nl-2-t" checked><span class="ed-text" id="nl-2-t" data-text>Pick three reader questions</span><input class="form-control ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn ed-ibtn" type="button" data-edit aria-label="Edit “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="btn ed-ibtn danger" type="button" data-delete aria-label="Delete “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="nl-3" aria-labelledby="nl-3-t"><span class="ed-text" id="nl-3-t" data-text>Find a header photo for the bakery story</span><input class="form-control ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn ed-ibtn" type="button" data-edit aria-label="Edit “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="btn ed-ibtn danger" type="button" data-delete aria-label="Delete “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="nl-4" aria-labelledby="nl-4-t"><span class="ed-text" id="nl-4-t" data-text>Check every link in the events list</span><input class="form-control ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn ed-ibtn" type="button" data-edit aria-label="Edit “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="btn ed-ibtn danger" type="button" data-delete aria-label="Delete “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="nl-5" aria-labelledby="nl-5-t"><span class="ed-text" id="nl-5-t" data-text>Schedule the send for Tuesday at 08:00</span><input class="form-control ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn ed-ibtn" type="button" data-edit aria-label="Edit “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="btn ed-ibtn danger" type="button" data-delete aria-label="Delete “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
  </ul>
  <div class="toast show ed-toast" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p class="mb-0" data-toast-msg></p><button class="btn ed-undo" type="button" data-undo>Undo</button><button class="btn ed-tx" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="visually-hidden" role="status" data-live></p>
</section></div>

<div><span class="lbl">Compact list with text buttons</span>
<section class="card ed compact" data-edit-list aria-labelledby="bug-title">
  <h2 class="mb-0" id="bug-title" tabindex="-1">Bug bash follow-ups</h2>
  <p class="ed-hint">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="list-group list-group-flush ed-list" data-list aria-label="Bug bash tasks">
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="bug-1" aria-labelledby="bug-1-t"><span class="ed-text" id="bug-1-t" data-text>Login button jumps on Safari 17</span><input class="form-control form-control-sm ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn btn-sm ed-tbtn" type="button" data-edit aria-label="Edit “Login button jumps on Safari 17”">Edit</button><button class="btn btn-sm ed-tbtn danger" type="button" data-delete aria-label="Delete “Login button jumps on Safari 17”">Delete</button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="bug-2" aria-labelledby="bug-2-t"><span class="ed-text" id="bug-2-t" data-text>Export ignores the date filter</span><input class="form-control form-control-sm ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn btn-sm ed-tbtn" type="button" data-edit aria-label="Edit “Export ignores the date filter”">Edit</button><button class="btn btn-sm ed-tbtn danger" type="button" data-delete aria-label="Delete “Export ignores the date filter”">Delete</button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="bug-3" aria-labelledby="bug-3-t" checked><span class="ed-text" id="bug-3-t" data-text>Typo on the pricing page</span><input class="form-control form-control-sm ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn btn-sm ed-tbtn" type="button" data-edit aria-label="Edit “Typo on the pricing page”">Edit</button><button class="btn btn-sm ed-tbtn danger" type="button" data-delete aria-label="Delete “Typo on the pricing page”">Delete</button></span></li>
    <li class="list-group-item ed-row"><input class="form-check-input" type="checkbox" id="bug-4" aria-labelledby="bug-4-t"><span class="ed-text" id="bug-4-t" data-text>Toast covers the cookie banner on phones</span><input class="form-control form-control-sm ed-field" type="text" data-field hidden maxlength="120"><span class="ed-acts" data-actions><button class="btn btn-sm ed-tbtn" type="button" data-edit aria-label="Edit “Toast covers the cookie banner on phones”">Edit</button><button class="btn btn-sm ed-tbtn danger" type="button" data-delete aria-label="Delete “Toast covers the cookie banner on phones”">Delete</button></span></li>
  </ul>
  <div class="toast show ed-toast" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p class="mb-0" data-toast-msg></p><button class="btn ed-undo" type="button" data-undo>Undo</button><button class="btn ed-tx" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="visually-hidden" role="status" data-live></p>
</section></div>
CSS
/* Inline editing and undo on Bootstrap: .card, .list-group, .form-check-input, .form-control, .btn and .toast. Each row is a checkbox named by its text (aria-labelledby), a hidden text field the script
   shows for editing, and Edit / Delete buttons named after the task. Deleting shows a message with an Undo button. */
.ed{--bs-card-border-color:var(--ed-line);--bs-card-border-radius:18px;--ed-ink:#0b2730;--ed-muted:#46606a;--ed-line:#d6e6ea;--ed-box:#6a8790;--ed-accent:#0e7490;--ed-accent-dark:#155e75;--ed-hover:#eef7f9;
  --ed-danger:#b42318;--ed-danger-bg:#fdecea;--ed-toast:#0b2730;--ed-toast-ink:#e6f4f7;--ed-undo:#a5f3fc;
  position:relative;background:#fff;color:var(--ed-ink);border:1px solid var(--ed-line);border-radius:18px;padding:24px 24px 20px;box-shadow:0 18px 40px -28px rgba(11,39,48,.4)}
.ed [hidden]{display:none!important}
.ed :where(h2,p,ul){margin:0;padding:0}
.ed ul{list-style:none}
.ed h2{font-size:1.2rem;font-weight:800;letter-spacing:-.015em}
.ed h2:focus{outline:none}
.ed-hint{margin-top:6px!important;font-size:.85rem;line-height:1.5;color:var(--ed-muted)}
.ed-hint kbd{font:inherit;font-size:.78rem;font-weight:700;padding:1px 6px;border:1px solid #b7cdd3;border-bottom-width:2px;border-radius:6px;background:#f6fafb;color:var(--ed-ink)}
.ed-list{--bs-list-group-border-color:var(--ed-line);--bs-list-group-color:var(--ed-ink);margin-top:14px!important;border-top:1px solid var(--ed-line);border-radius:0}
.ed .list-group-flush>.ed-row:last-child{border-bottom-width:1px}
.ed .ed-row{display:flex;align-items:center;gap:12px;min-height:56px;padding:8px 6px 8px 10px;border-bottom:1px solid var(--ed-line);transition:background-color .15s}
.ed-row:hover{background:var(--ed-hover)}
.ed .form-check-input{flex:none;width:20px;height:20px;margin:0;border:2px solid var(--ed-box);border-radius:6px;background-size:13px;cursor:pointer;
  transition:background-color .15s,border-color .15s}
.ed .form-check-input:checked{background-color:var(--ed-accent);border-color:var(--ed-accent)}
.ed .form-check-input:focus{box-shadow:none;border-color:var(--ed-box)}
.ed .form-check-input:checked:focus{border-color:var(--ed-accent)}
.ed .form-check-input:focus-visible{outline:3px solid var(--ed-accent);outline-offset:2px}
.ed-text{flex:1;min-width:0;font-size:1rem;line-height:1.4;overflow-wrap:anywhere;cursor:text;user-select:none}
.ed .form-check-input:checked+.ed-text{color:var(--ed-muted);text-decoration:line-through}
.ed .ed-field{flex:1;box-shadow:none;min-width:0;font:inherit;font-size:1rem;line-height:1.4;padding:6px 10px;margin:-1px 0;border:2px solid var(--ed-accent);border-radius:9px;background:#fff;color:inherit}
.ed-field:focus{outline:3px solid #a5e3ee;outline-offset:0}
.ed-acts{display:flex;gap:2px;flex:none}
.ed-ibtn{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:transparent;color:var(--ed-muted);cursor:pointer;transition:background-color .15s,color .15s}
.ed-ibtn:hover{background:#dff0f4;color:var(--ed-accent-dark)}
.ed-ibtn.danger:hover{background:var(--ed-danger-bg);color:var(--ed-danger)}
.ed .btn:focus-visible,.ed button:focus-visible{box-shadow:none;outline:3px solid var(--ed-accent);outline-offset:2px}
/* undo message */
.ed .ed-toast{--bs-toast-max-width:none;width:auto;font-size:inherit;border:0;display:flex;align-items:center;gap:10px;margin-top:14px;padding:10px 8px 10px 14px;border-radius:12px;background:var(--ed-toast);color:var(--ed-toast-ink);
  box-shadow:0 14px 30px -12px rgba(11,39,48,.55);animation:ed-in .22s ease-out}
.ed-toast>svg{flex:none;opacity:.8}
.ed-toast p{flex:1;min-width:0;font-size:.92rem;line-height:1.4;overflow-wrap:anywhere}
.ed-undo{flex:none;line-height:normal;font-size:.9rem;font-weight:800;padding:7px 12px;border:0;border-radius:8px;background:transparent;color:var(--ed-undo);cursor:pointer;transition:background-color .15s}
.ed-undo:hover{background:rgba(165,243,252,.14)}
.ed-tx{flex:none;display:grid;place-items:center;width:32px;height:32px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--ed-toast-ink);cursor:pointer}
.ed-tx:hover{background:rgba(255,255,255,.1)}
.ed .ed-toast button:focus-visible{outline-color:var(--ed-undo)}
@keyframes ed-in{from{opacity:0;transform:translateY(6px)}}
/* compact variant */
.ed.compact{padding:20px 20px 16px;border-radius:14px}
.ed.compact h2{font-size:1.05rem}
.ed.compact .ed-row{min-height:46px;padding:5px 4px 5px 8px}
.ed.compact .ed-text{font-size:.95rem}
.ed-tbtn{font-size:.8rem;line-height:normal;font-weight:700;padding:5px 9px;border:1px solid var(--ed-line);border-radius:8px;background:#fff;color:var(--ed-accent-dark);cursor:pointer;transition:background-color .15s,border-color .15s}
.ed-tbtn:hover{background:var(--ed-hover);border-color:#a9c8cf}
.ed-tbtn.danger{color:var(--ed-danger)}
.ed-tbtn.danger:hover{background:var(--ed-danger-bg);border-color:#f3b4ae}
.ed.compact .ed-acts{gap:6px}
.ed.compact .ed-field{font-size:.95rem;padding:4px 9px}
@media (max-width:480px){.ed{padding:20px 16px 16px}.ed-row{padding-left:6px}}
@media (prefers-reduced-motion:reduce){.ed *{transition:none!important;animation:none!important}}
JavaScript
/* Inline editing and undo. Double-click a task or press Edit to rename it: Enter saves, Esc cancels, leaving the
   field saves. Delete removes the row and shows a message with Undo for 8 seconds, paused while it has the pointer or focus. */
document.querySelectorAll('[data-edit-list]').forEach((root) => {
  const list = root.querySelector('[data-list]');
  const live = root.querySelector('[data-live]');
  const toast = root.querySelector('[data-toast]');
  const msg = toast.querySelector('[data-toast-msg]');
  const q = (s) => '“' + s + '”';
  const say = (m) => { live.textContent = ''; setTimeout(() => { live.textContent = m; }, 60); };
  const name = (li) => li.querySelector('[data-text]').textContent;
  const bits = (li) => ({ text: li.querySelector('[data-text]'), field: li.querySelector('[data-field]'), actions: li.querySelector('[data-actions]') });
  let removed = null, timer = 0;

  const relabel = (li) => {
    li.querySelector('[data-edit]').setAttribute('aria-label', 'Edit ' + q(name(li)));
    li.querySelector('[data-delete]').setAttribute('aria-label', 'Delete ' + q(name(li)));
  };
  const startEdit = (li) => {
    if ('editing' in li.dataset) return;
    const { text, field, actions } = bits(li);
    li.dataset.editing = '';
    field.value = text.textContent;
    field.setAttribute('aria-label', 'Edit ' + q(text.textContent));
    text.hidden = true; actions.hidden = true; field.hidden = false;
    field.focus(); field.setSelectionRange(field.value.length, field.value.length);
  };
  const endEdit = (li, keep, refocus) => {
    if (!('editing' in li.dataset)) return;
    delete li.dataset.editing;
    const { text, field, actions } = bits(li);
    const value = field.value.trim().replace(/\s+/g, ' ');
    if (!keep) say('Edit cancelled.');
    else if (!value) say('A task needs a name. Kept ' + q(text.textContent) + '.');
    else if (value !== text.textContent) { text.textContent = value; relabel(li); say('Saved ' + q(value) + '.'); }
    field.hidden = true; text.hidden = false; actions.hidden = false;
    if (refocus) li.querySelector('[data-edit]').focus();
  };

  const hideToast = () => { clearTimeout(timer); toast.hidden = true; removed = null; };
  const arm = () => { clearTimeout(timer); timer = setTimeout(hideToast, 8000); };
  const pause = () => clearTimeout(timer);
  const resume = (e) => { if (!toast.hidden && !toast.contains(e.relatedTarget) && !toast.matches(':hover')) arm(); };
  toast.addEventListener('mouseenter', pause);
  toast.addEventListener('focusin', pause);
  toast.addEventListener('mouseleave', resume);
  toast.addEventListener('focusout', resume);

  list.addEventListener('click', (e) => {
    const li = e.target.closest('li');
    if (e.target.closest('[data-edit]')) { startEdit(li); return; }
    if (!e.target.closest('[data-delete]')) return;
    const next = li.nextElementSibling || li.previousElementSibling;
    removed = { li, index: [...list.children].indexOf(li) };
    li.remove();
    msg.textContent = 'Deleted ' + q(name(li));
    toast.hidden = false; arm();
    (next ? next.querySelector('input[type=checkbox]') : toast.querySelector('[data-undo]')).focus();
    say('Deleted ' + q(name(li)) + '. Undo is in the message below the list.');
  });
  list.addEventListener('dblclick', (e) => {
    const text = e.target.closest('[data-text]');
    if (text) startEdit(text.closest('li'));
  });
  list.addEventListener('keydown', (e) => {
    if (!e.target.matches('[data-field]')) return;
    if (e.key === 'Enter') { e.preventDefault(); endEdit(e.target.closest('li'), true, true); }
    if (e.key === 'Escape') { e.preventDefault(); endEdit(e.target.closest('li'), false, true); }
  });
  list.addEventListener('focusout', (e) => { if (e.target.matches('[data-field]')) endEdit(e.target.closest('li'), true, false); });
  list.addEventListener('change', (e) => {
    if (e.target.type === 'checkbox') say(q(name(e.target.closest('li'))) + (e.target.checked ? ' done.' : ' not done.'));
  });

  toast.querySelector('[data-undo]').addEventListener('click', () => {
    if (!removed) return;
    const { li, index } = removed;
    list.insertBefore(li, list.children[index] || null);
    hideToast();
    li.querySelector('input[type=checkbox]').focus();
    say('Restored ' + q(name(li)) + '.');
  });
  toast.querySelector('[data-toast-close]').addEventListener('click', () => {
    hideToast();
    (list.querySelector('input[type=checkbox]') || root.querySelector('h2')).focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#46606a] uppercase">Edit button and double-click</span>
<section class="relative rounded-[18px] border border-[#d6e6ea] bg-white text-[#0b2730] shadow-[0_18px_40px_-28px_rgba(11,39,48,.4)] px-6 pt-6 pb-5 max-[480px]:px-4 max-[480px]:pt-5 max-[480px]:pb-4" data-edit-list aria-labelledby="nl-title">
  <h2 class="text-[1.2rem] font-extrabold tracking-[-.015em] focus:outline-none" id="nl-title" tabindex="-1">Newsletter, issue 42</h2>
  <p class="mt-1.5 text-[.85rem] leading-normal text-[#46606a] [&_kbd]:rounded-md [&_kbd]:border [&_kbd]:border-b-2 [&_kbd]:border-[#b7cdd3] [&_kbd]:bg-[#f6fafb] [&_kbd]:px-1.5 [&_kbd]:py-px [&_kbd]:font-[inherit] [&_kbd]:text-[.78rem] [&_kbd]:font-bold [&_kbd]:text-[#0b2730]">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="mt-3.5 border-t border-[#d6e6ea]" data-list aria-label="Newsletter tasks">
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-14 py-2 pr-1.5 pl-2.5 max-[480px]:pl-1.5"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="nl-1" aria-labelledby="nl-1-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-base" id="nl-1-t" data-text>Draft the intro paragraph</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-2.5 py-1.5 text-base" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-0.5" data-actions><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none hover:bg-[#fdecea]! hover:text-[#b42318]!" type="button" data-delete aria-label="Delete “Draft the intro paragraph”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-14 py-2 pr-1.5 pl-2.5 max-[480px]:pl-1.5"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="nl-2" aria-labelledby="nl-2-t" checked><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-base" id="nl-2-t" data-text>Pick three reader questions</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-2.5 py-1.5 text-base" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-0.5" data-actions><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none hover:bg-[#fdecea]! hover:text-[#b42318]!" type="button" data-delete aria-label="Delete “Pick three reader questions”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-14 py-2 pr-1.5 pl-2.5 max-[480px]:pl-1.5"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="nl-3" aria-labelledby="nl-3-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-base" id="nl-3-t" data-text>Find a header photo for the bakery story</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-2.5 py-1.5 text-base" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-0.5" data-actions><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none hover:bg-[#fdecea]! hover:text-[#b42318]!" type="button" data-delete aria-label="Delete “Find a header photo for the bakery story”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-14 py-2 pr-1.5 pl-2.5 max-[480px]:pl-1.5"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="nl-4" aria-labelledby="nl-4-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-base" id="nl-4-t" data-text>Check every link in the events list</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-2.5 py-1.5 text-base" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-0.5" data-actions><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none hover:bg-[#fdecea]! hover:text-[#b42318]!" type="button" data-delete aria-label="Delete “Check every link in the events list”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-14 py-2 pr-1.5 pl-2.5 max-[480px]:pl-1.5"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="nl-5" aria-labelledby="nl-5-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-base" id="nl-5-t" data-text>Schedule the send for Tuesday at 08:00</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-2.5 py-1.5 text-base" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-0.5" data-actions><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.5 2.5l3 3L5.5 13.5H2.5v-3z"/><path d="M9 4l3 3"/></svg></button><button class="grid size-9 cursor-pointer place-items-center rounded-[10px] text-[#46606a] transition-colors duration-150 hover:bg-[#dff0f4] hover:text-[#155e75] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none hover:bg-[#fdecea]! hover:text-[#b42318]!" type="button" data-delete aria-label="Delete “Schedule the send for Tuesday at 08:00”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg></button></span></li>
  </ul>
  <div class="mt-3.5 flex items-center gap-2.5 rounded-xl bg-[#0b2730] py-2.5 pr-2 pl-3.5 text-[#e6f4f7] shadow-[0_14px_30px_-12px_rgba(11,39,48,.55)] animate-ed-in motion-reduce:animate-none [&>svg]:flex-none [&>svg]:opacity-80" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p class="min-w-0 flex-1 text-[.92rem] leading-[1.4] [overflow-wrap:anywhere]" data-toast-msg></p><button class="flex-none cursor-pointer rounded-lg px-3 py-[7px] text-[.9rem] font-extrabold text-[#a5f3fc] transition-colors duration-150 hover:bg-[#a5f3fc]/15 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#a5f3fc] motion-reduce:transition-none" type="button" data-undo>Undo</button><button class="grid size-8 flex-none cursor-pointer place-items-center rounded-lg text-[#e6f4f7] hover:bg-white/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#a5f3fc]" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="sr-only" role="status" data-live></p>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#46606a] uppercase">Compact list with text buttons</span>
<section class="relative rounded-[14px] border border-[#d6e6ea] bg-white text-[#0b2730] shadow-[0_18px_40px_-28px_rgba(11,39,48,.4)] px-5 pt-5 pb-4 max-[480px]:px-4" data-edit-list aria-labelledby="bug-title">
  <h2 class="text-[1.05rem] font-extrabold tracking-[-.015em] focus:outline-none" id="bug-title" tabindex="-1">Bug bash follow-ups</h2>
  <p class="mt-1.5 text-[.85rem] leading-normal text-[#46606a] [&_kbd]:rounded-md [&_kbd]:border [&_kbd]:border-b-2 [&_kbd]:border-[#b7cdd3] [&_kbd]:bg-[#f6fafb] [&_kbd]:px-1.5 [&_kbd]:py-px [&_kbd]:font-[inherit] [&_kbd]:text-[.78rem] [&_kbd]:font-bold [&_kbd]:text-[#0b2730]">Double-click a task or press Edit to rename it. <kbd>Enter</kbd> saves, <kbd>Esc</kbd> cancels.</p>
  <ul class="mt-3.5 border-t border-[#d6e6ea]" data-list aria-label="Bug bash tasks">
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-[46px] py-[5px] pr-1 pl-2"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="bug-1" aria-labelledby="bug-1-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-[.95rem]" id="bug-1-t" data-text>Login button jumps on Safari 17</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-[9px] py-1 text-[.95rem]" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-1.5" data-actions><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Login button jumps on Safari 17”">Edit</button><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none text-[#b42318]! hover:border-[#f3b4ae]! hover:bg-[#fdecea]!" type="button" data-delete aria-label="Delete “Login button jumps on Safari 17”">Delete</button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-[46px] py-[5px] pr-1 pl-2"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="bug-2" aria-labelledby="bug-2-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-[.95rem]" id="bug-2-t" data-text>Export ignores the date filter</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-[9px] py-1 text-[.95rem]" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-1.5" data-actions><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Export ignores the date filter”">Edit</button><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none text-[#b42318]! hover:border-[#f3b4ae]! hover:bg-[#fdecea]!" type="button" data-delete aria-label="Delete “Export ignores the date filter”">Delete</button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-[46px] py-[5px] pr-1 pl-2"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="bug-3" aria-labelledby="bug-3-t" checked><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-[.95rem]" id="bug-3-t" data-text>Typo on the pricing page</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-[9px] py-1 text-[.95rem]" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-1.5" data-actions><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Typo on the pricing page”">Edit</button><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none text-[#b42318]! hover:border-[#f3b4ae]! hover:bg-[#fdecea]!" type="button" data-delete aria-label="Delete “Typo on the pricing page”">Delete</button></span></li>
    <li class="flex items-center gap-3 border-b border-[#d6e6ea] transition-colors duration-150 hover:bg-[#eef7f9] motion-reduce:transition-none min-h-[46px] py-[5px] pr-1 pl-2"><input class="peer size-5 flex-none cursor-pointer appearance-none rounded-md border-2 border-[#6a8790] bg-white bg-[length:13px] bg-center bg-no-repeat transition-colors duration-150 checked:border-[#0e7490] checked:bg-[#0e7490] checked:bg-tick focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="checkbox" id="bug-4" aria-labelledby="bug-4-t"><span class="min-w-0 flex-1 cursor-text leading-[1.4] [overflow-wrap:anywhere] select-none peer-checked:text-[#46606a] peer-checked:line-through text-[.95rem]" id="bug-4-t" data-text>Toast covers the cookie banner on phones</span><input class="min-w-0 flex-1 -my-px rounded-[9px] border-2 border-[#0e7490] bg-white leading-[1.4] focus:outline-3 focus:outline-offset-0 focus:outline-[#a5e3ee] px-[9px] py-1 text-[.95rem]" type="text" data-field hidden maxlength="120"><span class="flex flex-none gap-1.5" data-actions><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none" type="button" data-edit aria-label="Edit “Toast covers the cookie banner on phones”">Edit</button><button class="cursor-pointer rounded-lg border border-[#d6e6ea] bg-white px-[9px] py-[5px] text-[.8rem] font-bold text-[#155e75] transition-colors duration-150 hover:border-[#a9c8cf] hover:bg-[#eef7f9] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#0e7490] motion-reduce:transition-none text-[#b42318]! hover:border-[#f3b4ae]! hover:bg-[#fdecea]!" type="button" data-delete aria-label="Delete “Toast covers the cookie banner on phones”">Delete</button></span></li>
  </ul>
  <div class="mt-3.5 flex items-center gap-2.5 rounded-xl bg-[#0b2730] py-2.5 pr-2 pl-3.5 text-[#e6f4f7] shadow-[0_14px_30px_-12px_rgba(11,39,48,.55)] animate-ed-in motion-reduce:animate-none [&>svg]:flex-none [&>svg]:opacity-80" data-toast hidden><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.5 4.5h11M6.5 4.5V2.8h3v1.7M4 4.5l.7 9h6.6l.7-9"/></svg><p class="min-w-0 flex-1 text-[.92rem] leading-[1.4] [overflow-wrap:anywhere]" data-toast-msg></p><button class="flex-none cursor-pointer rounded-lg px-3 py-[7px] text-[.9rem] font-extrabold text-[#a5f3fc] transition-colors duration-150 hover:bg-[#a5f3fc]/15 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#a5f3fc] motion-reduce:transition-none" type="button" data-undo>Undo</button><button class="grid size-8 flex-none cursor-pointer place-items-center rounded-lg text-[#e6f4f7] hover:bg-white/10 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#a5f3fc]" type="button" data-toast-close aria-label="Dismiss message"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></div>
  <p class="sr-only" role="status" data-live></p>
</section></div>
CSS
/* the white tick drawn inside a checked box, and the undo message's entrance */
@utility bg-tick {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
}
@theme {
  --animate-ed-in: ed-in .22s ease-out;
  @keyframes ed-in { from { opacity: 0; transform: translateY(6px); } }
}
JavaScript
/* Inline editing and undo. Double-click a task or press Edit to rename it: Enter saves, Esc cancels, leaving the
   field saves. Delete removes the row and shows a message with Undo for 8 seconds, paused while it has the pointer or focus. */
document.querySelectorAll('[data-edit-list]').forEach((root) => {
  const list = root.querySelector('[data-list]');
  const live = root.querySelector('[data-live]');
  const toast = root.querySelector('[data-toast]');
  const msg = toast.querySelector('[data-toast-msg]');
  const q = (s) => '“' + s + '”';
  const say = (m) => { live.textContent = ''; setTimeout(() => { live.textContent = m; }, 60); };
  const name = (li) => li.querySelector('[data-text]').textContent;
  const bits = (li) => ({ text: li.querySelector('[data-text]'), field: li.querySelector('[data-field]'), actions: li.querySelector('[data-actions]') });
  let removed = null, timer = 0;

  const relabel = (li) => {
    li.querySelector('[data-edit]').setAttribute('aria-label', 'Edit ' + q(name(li)));
    li.querySelector('[data-delete]').setAttribute('aria-label', 'Delete ' + q(name(li)));
  };
  const startEdit = (li) => {
    if ('editing' in li.dataset) return;
    const { text, field, actions } = bits(li);
    li.dataset.editing = '';
    field.value = text.textContent;
    field.setAttribute('aria-label', 'Edit ' + q(text.textContent));
    text.hidden = true; actions.hidden = true; field.hidden = false;
    field.focus(); field.setSelectionRange(field.value.length, field.value.length);
  };
  const endEdit = (li, keep, refocus) => {
    if (!('editing' in li.dataset)) return;
    delete li.dataset.editing;
    const { text, field, actions } = bits(li);
    const value = field.value.trim().replace(/\s+/g, ' ');
    if (!keep) say('Edit cancelled.');
    else if (!value) say('A task needs a name. Kept ' + q(text.textContent) + '.');
    else if (value !== text.textContent) { text.textContent = value; relabel(li); say('Saved ' + q(value) + '.'); }
    field.hidden = true; text.hidden = false; actions.hidden = false;
    if (refocus) li.querySelector('[data-edit]').focus();
  };

  const hideToast = () => { clearTimeout(timer); toast.hidden = true; removed = null; };
  const arm = () => { clearTimeout(timer); timer = setTimeout(hideToast, 8000); };
  const pause = () => clearTimeout(timer);
  const resume = (e) => { if (!toast.hidden && !toast.contains(e.relatedTarget) && !toast.matches(':hover')) arm(); };
  toast.addEventListener('mouseenter', pause);
  toast.addEventListener('focusin', pause);
  toast.addEventListener('mouseleave', resume);
  toast.addEventListener('focusout', resume);

  list.addEventListener('click', (e) => {
    const li = e.target.closest('li');
    if (e.target.closest('[data-edit]')) { startEdit(li); return; }
    if (!e.target.closest('[data-delete]')) return;
    const next = li.nextElementSibling || li.previousElementSibling;
    removed = { li, index: [...list.children].indexOf(li) };
    li.remove();
    msg.textContent = 'Deleted ' + q(name(li));
    toast.hidden = false; arm();
    (next ? next.querySelector('input[type=checkbox]') : toast.querySelector('[data-undo]')).focus();
    say('Deleted ' + q(name(li)) + '. Undo is in the message below the list.');
  });
  list.addEventListener('dblclick', (e) => {
    const text = e.target.closest('[data-text]');
    if (text) startEdit(text.closest('li'));
  });
  list.addEventListener('keydown', (e) => {
    if (!e.target.matches('[data-field]')) return;
    if (e.key === 'Enter') { e.preventDefault(); endEdit(e.target.closest('li'), true, true); }
    if (e.key === 'Escape') { e.preventDefault(); endEdit(e.target.closest('li'), false, true); }
  });
  list.addEventListener('focusout', (e) => { if (e.target.matches('[data-field]')) endEdit(e.target.closest('li'), true, false); });
  list.addEventListener('change', (e) => {
    if (e.target.type === 'checkbox') say(q(name(e.target.closest('li'))) + (e.target.checked ? ' done.' : ' not done.'));
  });

  toast.querySelector('[data-undo]').addEventListener('click', () => {
    if (!removed) return;
    const { li, index } = removed;
    list.insertBefore(li, list.children[index] || null);
    hideToast();
    li.querySelector('input[type=checkbox]').focus();
    say('Restored ' + q(name(li)) + '.');
  });
  toast.querySelector('[data-toast-close]').addEventListener('click', () => {
    hideToast();
    (list.querySelector('input[type=checkbox]') || root.querySelector('h2')).focus();
  });
});

About this set

To-do lists where tasks can be renamed in place and a deleted task can be brought back. Edit button and double-click is a newsletter checklist with a pencil and a bin button on every row; Compact list with text buttons is a bug bash list with small Edit and Delete buttons. Double-clicking a task’s text or pressing Edit swaps the text for a field with the caret at the end: Enter saves, Escape cancels, and leaving the field saves too. An empty name is refused and the old one kept, and the row’s Edit and Delete buttons are renamed to match the new text. Delete removes the row and shows a dark message bar under the list with Undo and Dismiss buttons. Undo puts the task back in the same position and focuses its checkbox. The bar hides itself after eight seconds, but the timer pauses while the pointer or keyboard focus is on it, so there is time to reach Undo. About ninety lines of vanilla JavaScript handle both lists, and a short hint above each list says how editing works. The Bootstrap version uses .list-group, .form-control and a restyled .toast.

What’s included

  • Rename by double-click or the Edit button, Enter saves and Escape cancels
  • Edit and Delete buttons renamed after the task changes
  • Delete shows a message bar with Undo and Dismiss
  • Undo restores the task in its old position
  • Message timer pauses while it has the pointer or focus

Accessibility

Each checkbox is named by its task text through aria-labelledby, and the Edit and Delete buttons are named after the task, such as Delete “Typo on the pricing page”. Saving, cancelling, deleting and restoring are announced in a polite status region; after a delete focus moves to the next row, or to Undo when the list is empty, and after editing it returns to the Edit button. Every control has a visible focus outline, and the message bar's entrance animation stops under prefers-reduced-motion.

Customise it

Change --ed-accent, --ed-accent-dark, --ed-toast and --ed-undo on .ed for the list and the message bar. Bootstrap maps them onto .form-check-input, .form-control and .toast; in Tailwind swap the #0e7490, #0b2730 and #a5f3fc colour utilities.