DEV Community

Dhardingsea Developer
Dhardingsea Developer

Posted on

Twelve gradients, one id - DHSeaDev

A markup defect with no visual symptom, the audit that missed it five times, and the guard that stopped the fix from being worse than the bug.

The page in question: dhseadev.online/play


MARKUP · A DEFECT WITH NO SYMPTOM

Twelve gradients, one id

Eleven of them were dead. The page looked exactly right, and that was the problem.


A site scan flagged nothing. My own audit reported zero defects across forty-six pages.
The page rendered precisely as designed. Eleven of its twelve gradients had never been
used by anything.


THE DEFECT ————————————

What twelve copies of one emblem actually produces

I had an SVG emblem I liked, so I pasted it into a page twelve times. Each copy brought
its own gradient definition along with it.

<svg viewBox="0 0 40 40">
  <defs>
    <linearGradient id="pg">
      <stop stop-color="#C4B5FD"/>
      <stop stop-color="#93C5FD"/>
      <stop stop-color="#F0ABFC"/>
    </linearGradient>
  </defs>
  <path fill="url(#pg)" d="..."/>
</svg>
Enter fullscreen mode Exit fullscreen mode

Twelve <svg> blocks. Twelve elements carrying id="pg". Duplicate ids are invalid
HTML, and url(#pg) resolves to the first match in the document — so every emblem on
the page was painted by gradient number one, and the remaining eleven were parsed, held
in memory, and referenced by nothing.

The reason it stayed invisible is almost insulting: all twelve definitions held
identical stops. There was no visual symptom because there was nothing to see. A bug
like this waits patiently for the day you change one gradient and the change lands on
the wrong element, or on none of them.


THE AUDIT ————————————

Why five passes of my own tooling missed it

My checks looked for missing alt attributes, broken links, absent structured data.
"Are any ids duplicated" was not among them, because it had never yet cost me anything.

It costs four lines to add.

const ids = {};
for (const el of document.querySelectorAll('[id]')) {
  ids[el.id] = (ids[el.id] || 0) + 1;
}
console.log(Object.keys(ids).filter(k => ids[k] > 1));
Enter fullscreen mode Exit fullscreen mode

THE FIX ————————————

The trap sitting inside the repair

The obvious remedy is a unique id per gradient. My first attempt renamed sequentially
across the whole document, and I put a count assertion in front of it out of habit
rather than suspicion.

let n = 0;
html = html.replace(/id="pg"/g,    () => `id="pg${++n}"`);
let m = 0;
html = html.replace(/url\(#pg\)/g, () => `url(#pg${++m})`);
Enter fullscreen mode Exit fullscreen mode

The assertion failed.

defs = 12 · refs = 115

Each emblem defined one gradient and referenced it roughly ten times. A sequential
rename would have paired the second definition with the second reference — which
belonged to the first emblem. It would have converted an invisible defect into a
visibly broken page, and it would have done so while reporting success.

The correct repair scopes the rename to each <svg> block, so a definition can only
ever be paired with references from its own subtree.

let n = 0;
html = html.replace(/<svg[\s\S]*?<\/svg>/g, (block) => {
  if ((block.match(/id="pg"/g) || []).length !== 1) return block;
  const id = `pg${++n}`;
  return block
    .split('id="pg"').join(`id="${id}"`)
    .split('url(#pg)').join(`url(#${id})`);
});
Enter fullscreen mode Exit fullscreen mode

Twelve blocks renamed. Zero leftover definitions, zero leftover references, twelve
distinct ids, and no duplicate id anywhere on the page.


WHAT IT LEAVES BEHIND ————————————

Inline SVG pasted more than once duplicates every internal id it carries — not only
gradients, but filters, masks, clipPaths, patterns, and every aria-labelledby target.
A duplicate-id check belongs in the audit precisely because this class of bug produces
nothing to look at.

And a bulk find-and-replace that assumes definitions and references pair up should be
made to prove it before it writes a single byte. Mine was guarded by habit rather than
foresight, and habit was the only reason a twelve-against-one-hundred-and-fifteen
mismatch never shipped.


Found while auditing dhseadev.online, August 2026.

Top comments (0)