The del tag, ins and s: crossed-out text that means something

Three HTML elements draw a line through text, and they say different things. del and ins record an edit. s marks something that is no longer accurate.

The <del> tag marks text that was removed from a document, and <ins> marks text that was added. Together they record an edit: the old words stay visible with a line through them, and the new words sit next to them.

For a price that is no longer valid, use <s> instead.

<p>The launch moves from <del>Friday</del> <ins>Monday</ins>.</p>

Try it below. Show dates reads the hidden datetime attribute. Clean copy hides the deletions, which is the document as it reads now.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>del and ins</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .doc { background: #fff; border-radius: 12px; padding: 14px 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); line-height: 1.55; }
  h2 { font-size: 17px; margin: 0 0 6px; }
  p { margin: 0; }
  ul { margin: 8px 0 0; padding-left: 20px; }
  del { color: #9a3412; background: #fdecea; }
  ins { color: #0f5132; background: #e3f6e8; }
  /* the browser does not show datetime; this reads it from the attribute */
  .dates del::after, .dates ins::after {
    content: attr(datetime);
    display: inline-block;  /* keeps the strike line off the label */
    margin-left: 4px; padding: 0 5px; border-radius: 4px;
    font: 12px ui-monospace, Consolas, monospace; background: #eef1f5; color: #475569;
  }
  .clean del { display: none; }  /* clean copy: hide what was removed */
  .clean ins { background: none; color: inherit; text-decoration: none; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
  button { font: inherit; font-size: 14px; padding: 7px 12px; border: 1px solid #cfd5dd; border-radius: 8px; background: #fff; cursor: pointer; }
  button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
</style>
</head>
<body>
<div class="doc" id="doc">
  <h2>Launch notes</h2>
  <p>The launch moves from
    <del datetime="2026-09-24T09:30Z" cite="https://example.com/minutes/42">Friday</del>
    <ins datetime="2026-09-24T09:30Z" cite="https://example.com/minutes/42">Monday</ins>.</p>
  <ul>
    <li>Final copy review</li>
    <li><del datetime="2026-09-25">Beta badge on the pricing page</del></li>
    <li><ins datetime="2026-09-25">Press kit link in the footer</ins></li>
  </ul>
</div>
<div class="bar">
  <button id="dates" aria-pressed="false">Show dates</button>
  <button id="clean" aria-pressed="false">Clean copy</button>
</div>

<script>
  const doc = document.getElementById('doc');
  // each button toggles one class on the document
  ['dates', 'clean'].forEach((name) => {
    const btn = document.getElementById(name);
    btn.addEventListener('click', () => {
      const on = doc.classList.toggle(name);
      btn.setAttribute('aria-pressed', on);
    });
  });
</script>
</body>
</html>
Two edits recorded with del and ins. Both buttons are one class each on the document.

del, ins, s or CSS: pick by meaning

All three of <del>, <s> and CSS line-through draw the same line. What differs is what the page says to anything that reads the markup: scripts, assistive technology, and the next person who edits the file.

The same line through text, three different meanings.
The same line through text, three different meanings.
Markup Meaning Typical use Default look
<del> Removed in an edit A changed date in minutes, a dropped line in a contract draft Line through
<ins> Added in an edit The replacement text Underline
<s> No longer accurate or relevant An old price, a sold-out ticket tier Line through
line-through in CSS No meaning, only a look A finished to-do item Line through
<strike> Obsolete, do not use Old pages Line through

A quick test: if you would say "we changed this", it is <del> and <ins>. If you would say "this used to be true", it is <s>. If nobody needs to know either way, it is CSS.

The CSS side, including line colour and thickness, is covered in text-decoration in CSS.

The datetime and cite attributes

<del> and <ins> take two optional attributes. Neither appears on the page, so they cost nothing visually.

datetime says when, cite says where the reason is written down. The reader sees neither.
datetime says when, cite says where the reason is written down. The reader sees neither.
  • datetime is when the change was made. It takes a date (2026-09-24) or a date and time. A time must carry a zone: Z for UTC or an offset such as +09:00. 2026-09-24T09:30 without one is not valid here.
  • cite is the URL of a page that explains the change, for example the meeting minutes or a ticket. Browsers do not turn it into a link.

Scripts read both through the element's dateTime and cite properties. To show the date on screen, as the first example does, CSS can print the attribute:

del::after { content: attr(datetime); display: inline-block; }

The display: inline-block keeps the strike line off the label. Without it, the line from <del> runs through the date too.

A strikethrough price that makes sense out loud

On a sale price, <s> is the right element: the old price is still shown on purpose, it is simply no longer the price.

The line alone is a visual cue, though. In text form, the card reads "$49$29 / month", with no hint which price applies.

Press the button to see the text of each card with the styling taken away.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Strikethrough price</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
  .card { background: #fff; border-radius: 12px; padding: 14px 16px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  .label { font-size: 12px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .3px; }
  h3 { margin: 6px 0 4px; font-size: 16px; }
  .price { font-size: 26px; font-weight: 800; }
  .old { font-size: 16px; font-weight: 400; color: #64748b; margin-right: 6px; }
  .fake { text-decoration: line-through; }  /* card A: the line is only styling */
  /* hidden on screen, still part of the page text */
  .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  button { margin-top: 14px; font: inherit; font-size: 14px; padding: 7px 12px; border: 1px solid #cfd5dd; border-radius: 8px; background: #fff; cursor: pointer; }
  pre { white-space: pre-wrap; margin: 10px 0 0; padding: 10px 12px; border-radius: 8px; background: #1d2330; color: #e5e7eb; font: 13px/1.5 ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="row">
  <div class="card" id="a">
    <div class="label">A: CSS line only</div>
    <h3>Pro plan</h3>
    <div class="price"><span class="old fake">$49</span>$29 <small>/ month</small></div>
  </div>
  <div class="card" id="b">
    <div class="label">B: s + hidden words</div>
    <h3>Pro plan</h3>
    <div class="price"><s class="old"><span class="sr">Was </span>$49</s><span class="sr">, now </span>$29 <small>/ month</small></div>
  </div>
</div>
<button id="read">Show the text without styling</button>
<pre id="out" hidden></pre>

<script>
  // The strike line is styling. This is the text that is left without it.
  const flat = (card) => card.querySelector('h3').textContent + ': ' +
    card.querySelector('.price').textContent.replace(/\s+/g, ' ').trim();

  document.getElementById('read').addEventListener('click', () => {
    const out = document.getElementById('out');
    out.textContent = 'A  ' + flat(document.getElementById('a')) + '\n' +
                      'B  ' + flat(document.getElementById('b'));
    out.hidden = false;
  });
</script>
</body>
</html>
Card A has only a CSS line. Card B uses s plus two short phrases that are hidden on screen.

<del> and <ins> carry the ARIA roles deletion and insertion, but whether a screen reader says anything about them depends on the reader and its settings. When the difference matters, put it in words. A visually hidden span does that without changing the design:

<s><span class="sr">Was </span>$49</s><span class="sr">, now </span>$29
.sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
The line is only styling. The hidden words carry the meaning into the text.
The line is only styling. The hidden words carry the meaning into the text.

Do not use display: none for these words. It removes them from the accessibility tree as well as from the screen.

Where del and ins can go

Both elements are unusual: they can wrap a single word inside a sentence, or several whole paragraphs. What they contain must still be allowed where they sit.

  • Inside a sentence: wrap only the words that changed.
  • Around blocks: <del><p>...</p><p>...</p></del> removes two paragraphs in one element.
  • In lists: a <ul> takes only <li> children, so put <del> inside the <li>, as the first example does.
  • In tables: do the same inside each cell. A <del> cannot wrap a table row.

A finished example: track changes between two versions

Writing <del> and <ins> by hand is fine for one edit. For a whole paragraph, let a script compare the two versions and write the markup. Edit either box and the result updates. Markup shows the HTML it produced.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Track changes with del and ins</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; }
  label { font-size: 13px; font-weight: 700; color: #475569; }
  textarea { box-sizing: border-box; width: 100%; height: 92px; margin-top: 4px; padding: 8px; font: 14px/1.4 system-ui, sans-serif; border: 1px solid #cfd5dd; border-radius: 8px; resize: vertical; }
  .bar { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
  button { font: inherit; font-size: 14px; padding: 7px 11px; border: 1px solid #cfd5dd; border-radius: 8px; background: #fff; cursor: pointer; }
  button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  #result { background: #fff; border-radius: 10px; padding: 12px 14px; line-height: 1.6; min-height: 48px; box-shadow: 0 4px 16px rgba(0, 0, 0, .08); }
  del { color: #9a3412; background: #fdecea; }
  ins { color: #0f5132; background: #e3f6e8; }
  del + ins { margin-left: 3px; }  /* a gap between old and new */
  #result.clean del { display: none; }
  #result.clean ins { background: none; color: inherit; text-decoration: none; }
  pre { white-space: pre-wrap; word-break: break-word; margin: 0; font: 12.5px/1.5 ui-monospace, Consolas, monospace; }
  .count { font-size: 13px; color: #475569; margin-top: 8px; }
</style>
</head>
<body>
<div class="cols">
  <div><label for="before">Before</label>
    <textarea id="before">The workshop starts at 10:00 on Friday in room 4. Bring a laptop.</textarea></div>
  <div><label for="after">After</label>
    <textarea id="after">The workshop starts at 13:00 on Monday in room 4. Bring a laptop and a charger.</textarea></div>
</div>
<div class="bar">
  <button data-view="changes" aria-pressed="true">Changes</button>
  <button data-view="clean" aria-pressed="false">Clean copy</button>
  <button data-view="markup" aria-pressed="false">Markup</button>
</div>
<div id="result"></div>
<div class="count" id="count"></div>

<script>
  const before = document.getElementById('before');
  const after = document.getElementById('after');
  const result = document.getElementById('result');
  let view = 'changes';

  // Word-level diff: longest common subsequence of two token lists.
  function diff(a, b) {
    const n = a.length, m = b.length;
    const L = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
    for (let i = n - 1; i >= 0; i--)
      for (let j = m - 1; j >= 0; j--)
        L[i][j] = a[i] === b[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
    const ops = [];
    let i = 0, j = 0;
    while (i < n || j < m) {
      if (i < n && j < m && a[i] === b[j]) { ops.push(['same', a[i]]); i++; j++; }
      else if (i < n && (j === m || L[i + 1][j] >= L[i][j + 1])) { ops.push(['del', a[i]]); i++; }
      else { ops.push(['ins', b[j]]); j++; }
    }
    return ops;
  }

  function render() {
    // split into words, spaces and punctuation, keeping all of them
    const words = (s) => s.split(/(\s+|[.,;!?])/).filter(Boolean);
    const ops = diff(words(before.value), words(after.value));
    const stamp = new Date().toISOString().slice(0, 16) + 'Z';  // e.g. 2026-09-26T10:00Z
    const out = document.createElement('p');
    out.style.margin = 0;
    let dels = 0, inss = 0;

    // join neighbouring words of the same kind into one element
    ops.forEach(([kind, text]) => {
      const last = out.lastChild;
      if (kind === 'same') { out.append(text); return; }
      if (last && last.localName === kind) { last.textContent += text; return; }
      const el = document.createElement(kind);
      el.setAttribute('datetime', stamp);
      el.textContent = text;
      out.append(el);
      kind === 'del' ? dels++ : inss++;
    });
    out.normalize();  // merge neighbouring text nodes

    result.className = view === 'clean' ? 'clean' : '';
    if (view === 'markup') {
      const pre = document.createElement('pre');
      pre.textContent = out.innerHTML;
      result.replaceChildren(pre);
    } else {
      result.replaceChildren(out);
    }
    document.getElementById('count').textContent = dels + ' deletion(s), ' + inss + ' insertion(s)';
  }

  document.querySelectorAll('[data-view]').forEach((btn) => {
    btn.addEventListener('click', () => {
      view = btn.dataset.view;
      document.querySelectorAll('[data-view]').forEach((b) => b.setAttribute('aria-pressed', b === btn));
      render();
    });
  });
  before.addEventListener('input', render);
  after.addEventListener('input', render);
  render();
</script>
</body>
</html>
A word-level diff that wraps removed words in del and added words in ins, each stamped with datetime.

How it works, in short:

  1. Split both texts into words, spaces and punctuation.
  2. Find the longest run of tokens the two versions share, in order.
  3. Everything only in the old text becomes <del>, everything only in the new text becomes <ins>.
  4. Neighbouring tokens of the same kind join into one element, and each gets the current time as datetime.

The script builds elements with createElement and textContent, so anything typed into the boxes stays plain text. That matters when the input comes from other people. For a published record of changes over time, see HTML for changelogs.

When it does not work

What you see Cause Fix
New text is underlined and looks like a link Browsers underline <ins> by default ins { text-decoration: none; } and give it a colour
The date shows nothing datetime is never displayed Read it with attr(datetime) or el.dateTime
The date label has a line through it ::after inherits the line from <del> display: inline-block on the ::after
A validator flags <strike> The element is obsolete Use <del> or <s>
A validator flags <del> in a list <del> is a direct child of <ul> Move it inside the <li>
Hidden "Was" text is missing in a screen reader The span uses display: none Use the .sr clip pattern instead

A marked-up edit is easiest to review in the browser, where the colours and the clean-copy toggle work. A screenshot freezes one view, and an .html attachment may open as raw code on the other side.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so reviewers can switch between changes and the clean copy themselves. If you update the text later, the same link shows the new version.

Questions people ask

What is the difference between del and s?

del marks text that was removed when the document was edited. s marks text that is still shown on purpose but is no longer accurate or relevant, such as an old price next to a new one. Browsers draw both with a line through them, so the difference is in the meaning, not the look.

Is the strike tag still valid HTML?

No. <strike> is obsolete in the HTML standard. Browsers still parse it and still draw a line through it, but a validator will flag it. Use del for an edit, s for something no longer accurate, or CSS text-decoration: line-through for a look with no meaning.

Does the datetime attribute show on the page?

No. Browsers do not display datetime or cite. They are there for scripts, tools and people reading the source. To show the date, read it with JavaScript (element.dateTime) or with CSS content: attr(datetime) in a ::after rule.

How do I remove the underline from ins?

Set ins { text-decoration: none; } in your CSS. Browsers underline ins by default. Give it another visual cue instead, such as a green background, so readers can still tell the added text apart.

Can del or ins wrap a whole paragraph?

Yes. Both can wrap a single word or several block elements. They cannot sit where only certain children are allowed: a ul or ol takes only li children, so put del inside the li, not around it.

Keep reading