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
];
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
);
});
});
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" },
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>
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 }) });
}
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: "'" },
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 µ instead of µ — 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
- HTML特殊符號對照表 — 繁體中文
- HTML特殊符号对照表 — 简体中文
- HTML Symbol Entity Reference — English
- HTML特殊記号対照表 — 日本語
- HTML 특수문자 코드 대조표 — 한국어
- Tableau de référence des entités HTML — Français
- Справочник спецсимволов HTML — Русский
- HTML-Sonderzeichenreferenz — Deutsch
- Tabel Referensi Simbol HTML — Bahasa Indonesia
- Tabla de símbolos HTML — Español
- Bảng ký tự đặc biệt HTML — Tiếng Việt
- ตารางอ้างอิงสัญลักษณ์ HTML — ไทย
- Tabela znaków specjalnych HTML — Polski
- HTML Özel Karakterler Referansı — Türkçe
- Tabella di riferimento simboli HTML — Italiano
- Tabela de Símbolos HTML — Português
- Tabel met HTML Speciale Tekens — Nederlands
- Довідник символів HTML — Українська
Top comments (0)