Decorative text looks simple.
Copy a few symbols, add them to a username or bio, and paste the result into a profile. Done.
Until the platform rejects the name.
Or the character counter says 11 when the user sees one emoji.
Or the cursor jumps into the middle of a joined emoji sequence.
Or a screen reader announces a row of symbols without explaining what the control does.
Emoji, kaomoji, and Unicode symbols are not just visual decoration. They are text, and text has structure. If you build tools that search, count, edit, validate, or copy decorative text, a few Unicode details make a big difference.
This article presents a practical mental model and a small set of implementation rules for building better text tools on the web.
1. A visible character is not always one code point
JavaScript strings are sequences of UTF-16 code units. That is already different from Unicode code points, and both are different from what a user perceives as a character.
Consider this family emoji:
const text = "👨👩👧👦";
console.log(text.length);
// 11 UTF-16 code units
console.log(Array.from(text).length);
// 7 Unicode code points
To a user, it is one visible character. Unicode calls that user-perceived unit an extended grapheme cluster.
For user-facing counts and cursor movement, grapheme clusters are usually the better unit:
const segmenter = new Intl.Segmenter(undefined, {
granularity: "grapheme",
});
const graphemes = [...segmenter.segment(text)];
console.log(graphemes.length);
// 1
The distinction matters for more than emoji. It also affects:
- accented letters made from a base letter and a combining mark;
- flags made from regional indicator symbols;
- skin-tone modifiers;
- emoji joined with zero-width joiners;
- scripts where a visual unit contains several code points.
Unicode’s Text Segmentation specification describes the rules behind these boundaries. The practical takeaway is simple: do not assume that string.length means “how many characters the user sees.”
2. Count according to the job
There is no single universally correct definition of “length.” The right count depends on what you are trying to protect.
| Job | Useful unit |
|---|---|
| JavaScript storage size | UTF-16 code units |
| Unicode-aware iteration | Code points |
| User-facing character count | Grapheme clusters |
| A platform username limit | That platform’s documented rule |
| Network payload size | Encoded bytes |
This is why a compatibility checker should not display a single green “safe” badge based only on JavaScript length. A platform may count characters differently, reject particular scripts, reserve symbols, or apply separate rules to different fields.
For an editor, grapheme-aware counting is a good default:
function countGraphemes(value) {
const segmenter = new Intl.Segmenter(undefined, {
granularity: "grapheme",
});
return [...segmenter.segment(value)].length;
}
But that count should still be labeled honestly. “1 visible character” is not the same as “accepted by every platform.”
3. Variation selectors change presentation
Some symbols have both text and emoji presentations. Variation selectors can influence how a sequence is rendered.
For example, these may look different depending on the sequence and the font:
♥
♥️
The second version includes a variation selector requesting emoji-style presentation. Both are valid text, but they are not identical strings.
This creates several design questions:
- Should search treat the two forms as equivalent?
- Should a copy tool preserve the original sequence?
- Should normalization happen before saving or only for comparison?
- Does the target platform support the requested presentation?
For a copy-and-paste tool, preserving the user’s chosen text is usually safer than silently rewriting it. You can normalize or create comparison keys for search, but the copied value should remain predictable.
4. Zero-width joiners are invisible but meaningful
The zero-width joiner, or ZWJ, is an invisible code point used in sequences such as family emoji and profession emoji. It connects visible components without adding visible width.
That makes ZWJ sequences especially easy to mishandle. A naive sanitizer may remove the joiner and turn one emoji into several unrelated emoji. A naive character counter may count every component separately. A naive cursor implementation may allow deletion from the middle of a sequence.
When editing decorative text:
- Segment the string into grapheme clusters.
- Move the cursor by grapheme cluster where possible.
- Delete a complete cluster rather than an arbitrary code unit range.
- Preserve valid sequences during copy and export.
The browser’s built-in text controls handle much of this interaction for ordinary editing. Problems often appear when an app renders every symbol as an individual custom button or implements its own text manipulation logic.
5. Copy the text, not a screenshot
If the user wants to paste a name, bio, status, or message, the output should be real text.
The modern Clipboard API is straightforward:
async function copyText(value) {
try {
await navigator.clipboard.writeText(value);
return true;
} catch {
return false;
}
}
There are still reasons to provide a fallback. Clipboard permission may be denied, the page may not be in a secure context, or a browser may block the action because it was not triggered by a user gesture.
A good fallback is not a confusing error page. It can be a focused manual-selection state that shows the exact text and explains what to do next.
Also, give the user clear feedback after a successful copy. “Copied” is more useful than changing an icon with no explanation.
6. Accessibility starts with the control, not the glyph
Decorative text is visual, but the surrounding interface still needs to be accessible.
This button is ambiguous:
<button>⧉</button>
This one explains the action:
<button type="button" aria-label="Copy this kaomoji">
⧉
</button>
Even better, use a visible label when the layout allows it:
<button type="button">
<span aria-hidden="true">⧉</span>
<span>Copy</span>
</button>
The same principle applies to favorite, remove, move, and compatibility actions. The symbol is decoration; the accessible name should describe the action.
Do not rely on a glyph alone to communicate status either. A toast such as “Copied to clipboard” or a polite live-region update gives the user a clear result.
7. Compatibility is a field-level problem
“Does this symbol work on Discord?” is not precise enough.
Many platforms have multiple text fields, and those fields can have different rules. A unique username, a display name, a profile bio, and a message may all behave differently.
A useful checker should ask:
- Which platform?
- Which field?
- What is the candidate text?
- What rule is being evaluated?
- When was the rule last checked?
The output should be conservative. A result such as “passes this reference check” is more trustworthy than “guaranteed to work everywhere.” Platform rules, moderation systems, fonts, devices, and client versions can all change the final result.
When the exact rule is unknown, say so. Uncertainty is better than a false green badge.
8. Treat invisible and confusable characters carefully
Unicode includes characters that are invisible, directional, combining, or visually similar to other characters. These capabilities are useful for legitimate languages and typography, but they can also create confusing or deceptive names.
For tools that decorate names or generate bios, consider showing a warning when text contains:
- bidirectional control characters;
- unexpected zero-width characters;
- isolated combining marks;
- characters outside the selected compatibility profile;
- visually confusable characters in an identity field.
Do not automatically delete every unusual character. That can corrupt valid writing. Instead, make the risk visible and offer a plain-text alternative when appropriate.
A practical product pattern is to provide three outputs:
- Creative version: preserves the requested decoration.
- Conservative version: removes or avoids higher-risk characters.
- Plain fallback: uses ordinary text and common emoji only.
This gives users a choice without pretending that one string is universally safe.
9. Search should understand intent, not only exact names
If you are building a catalog of symbols or kaomoji, search quality matters as much as the size of the catalog.
Users search by intent:
happy facecute dividerlove statusaesthetic bio symbols
Your data may use different labels. A useful local search layer can combine:
- item names;
- descriptions or notes;
- tags;
- aliases for common phrases;
- a small relevance score;
- a trend or popularity score as a tie-breaker.
You do not need a hosted search service for every catalog. For a read-only collection, an explainable client-side ranking function can be fast, private, and easy to improve.
The important part is to separate search matching from the copied value. Search can normalize a query for comparison, while copy should return the exact stored text.
10. A small tool can still be privacy-friendly
Decorative text tools often do not need accounts or server-side storage.
Favorites, recent items, and draft combinations can stay in the current browser. If you use local storage, handle the failure path explicitly:
- private browsing may restrict storage;
- users may block storage;
- old saved data may have an outdated shape;
- imported JSON should be validated;
- copying should still work when persistence fails.
If analytics are enabled, action-only events are usually enough to understand product behavior. You can measure that a copy action happened without sending the user’s name, bio, favorites, or copied text.
A practical tool for experimenting with this text
If you want to browse ready-to-copy emoji, kaomoji, cute symbols, and text combinations, Emojicons is a small tool I built for exactly that workflow.
It lets you search and copy individual items, collect several pieces into a combination, and use optional tools for names, bios, and compatibility checks. The useful part for this article is that the output stays as text—you can copy it into the destination where you actually plan to use it.
It is also a convenient way to test edge cases manually: long kaomoji, joined emoji, symbols with variation selectors, and combinations that behave differently across editors and platforms.
A compact checklist
Before shipping a text tool that handles decorative Unicode, check the following:
- Count grapheme clusters for user-facing character counts.
- Do not confuse code units, code points, graphemes, and platform limits.
- Preserve the exact copied value unless the user explicitly asks for transformation.
- Use accessible names for icon-only controls.
- Provide clipboard failure feedback and a manual fallback.
- Treat compatibility as platform-and-field-specific.
- Surface risky invisible or confusable characters carefully.
- Keep search normalization separate from output text.
- Make local storage optional rather than required.
- Avoid claiming that a candidate is guaranteed to work everywhere.
Final thought
The web has made it easy to copy expressive text, but not always easy to understand what that text contains.
The best tools respect both sides of the problem:
- the user sees a character;
- the runtime sees a sequence;
- the target platform applies its own rules.
Once you design around those three realities, emoji and Unicode stop being mysterious edge cases. They become ordinary text engineering—just with better test data.
Further reading
This article was originally published by DEV Community and written by Qpphello1.
Read original article on DEV Community