DEV Community

BeGoodTool.com
BeGoodTool.com

Posted on

Why three "characters" in my HTML symbol table aren't actually there

I built a click-to-copy table of HTML special characters — the kind of reference page you land on from a Google search when you need — or © and don't want to remember the numeric code. I figured the hard part would be assembling a few hundred symbols. It wasn't. The hard part was three rows where the "symbol" you're supposed to click on is invisible, plus a search box that only works if you already know what you're looking for.

The table is a flat array, not a category system

The marketing copy on the page talks about symbols being "neatly organized by category" — arrows, currency, math, Greek letters. In the actual data file, there's no category field anywhere. It's one flat array of ~360 plain objects:

let table = [
  { symbol: "Α", name: "Α", code: "Α" },
  { symbol: "", name: "←", code: "←" },
  { symbol: "¢", name: "¢", code: "¢" },
  { symbol: "©", name: "©", code: "©" },
  // ...roughly 360 of these
];
Enter fullscreen mode Exit fullscreen mode

The "categories" you notice while scrolling are just an artifact of the order the list was assembled in — Greek letters cluster because whoever built the table pasted them in as a block, not because there's a category: "greek" key anywhere. Filtering is a plain substring match across all three fields:

let showTable = computed(() => {
  return htmlsymbols.value.filter((item) => {
    return (
      item.symbol.indexOf(keyword.value) > -1 ||
      item.name.indexOf(keyword.value) > -1 ||
      item.code.indexOf(keyword.value) > -1
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Which means typing "arr" finds ←, →, ↑ — because those entity names literally contain "arr" — but typing "arrow" finds nothing, because the string "arrow" doesn't appear anywhere in the data. There's no synonym layer, no description field, no fuzzy matching. The search only works if you already half-know HTML entity naming conventions, which is a little backwards for a tool aimed at people who don't.

Making invisible characters clickable

Three rows in the table are spaces —  ,  ,   — and a plain space rendered in a table cell gives you nothing to click on. No visual weight, no hover target you'd notice, nothing. So those three rows carry an extra field the rest of the table doesn't have:

{ symbol: " ", name: " ", code: " ", rmk: "en space" },
{ symbol: " ", name: " ", code: " ", rmk: "em space" },
{ symbol: " ", name: " ", code: " ", rmk: "non-breaking" },
Enter fullscreen mode Exit fullscreen mode

And the template checks for it before deciding what to display:

<div class="tableGroup__item__symbol" :class="{ canCopy: item.symbol }" @click="copy(item.symbol)">
  {{ item.rmk ? `(${item.rmk})` : item.symbol || " " }}
</div>
Enter fullscreen mode Exit fullscreen mode

The label you see is (non-breaking), but the click handler is still wired to item.symbol — the actual invisible space character, not the label text. So the displayed text and the copied text are deliberately different things. It's a small trick, but it's the only reason those three rows are usable at all instead of being dead, empty-looking cells nobody would think to click.

Not every cell is copyable, and one code is just wrong

Each of the three columns — symbol, entity name, decimal code — copies independently, and the canCopy styling (and the click handler itself) is gated on the value actually being truthy:

function copy(content) {
  if (!content) return;
  navigator.clipboard.writeText(content ? content : res.value);
  message.info({ content: t("message.copySuccess", { copySuccess: content }) });
}
Enter fullscreen mode Exit fullscreen mode

That res.value fallback in the ternary is dead code — res isn't defined anywhere in this component, and it's unreachable anyway because the if (!content) return; guard already exits before you'd get there. Looks like a leftover from a copy-pasted pattern used in one of the other tools on the site that never got cleaned up here; harmless, but a good reminder that "it works" isn't the same as "every line does something."

The guard matters for real data, though — the apostrophe row has no named entity at all:

{ symbol: "'", name: "", code: "&#39;" },
Enter fullscreen mode Exit fullscreen mode

name is an empty string, so that cell never gets the canCopy class and clicking it does nothing — correctly, since HTML never defined a named entity for a plain apostrophe.

There's also a plain typo sitting in the data: the micro sign's decimal code is written &#181 instead of &#181; — missing the closing semicolon that every other row in the table has. Copy that one into an HTML document and depending on what follows it in your markup, it may not render as µ at all, because the semicolon terminator got dropped.

Where this actually falls short

  • Search is pure substring matching on exactly what's stored — no semantic search, no "symbols that look like an X."
  • The "clean categories" description in the page copy doesn't match the underlying data structure — there's no category field to filter by, just scroll order.
  • It only covers the classic HTML4/Latin-1/general-punctuation/Greek/math/arrows range — no emoji, no CJK punctuation, no anything from more recent Unicode blocks.
  • The invisible-character trick (rmk) only exists for three rows; any other zero-width or hard-to-see character added later would need the same manual override or it'd be effectively unclickable.

I cleaned up the version I use into a small free tool if you need to grab an entity code without digging through W3Schools tables: HTML Symbol Entity Reference. No sign-up, click any cell to copy it.


Available in other languages

Top comments (0)