Case Study

Unicode Studio

30+ transformations p50 ~1.2s (Fast 4G + 4x CPU) Static site <150KB gzipped

I like the subtle energy styled text can add to a line. Bold names, softer script headings, bubble text in bios. What I don’t like is how most tools deliver it: export an image, break copy‑paste, or lock me into a specific platform.

So I built the tool I wanted: type plain text once, explore a few looks, see how they behave in realistic layouts, and leave with Unicode characters that act like normal text almost everywhere.


The Itch

The idea clicked when I noticed how often I was copying styled text from somewhere, pasting it into another tool, and discovering it either broke, looked wrong, or wasn’t editable anymore.

The existing generators were either toys or black boxes. They rarely showed how the text would look in context, and getting back to a “normal” version was usually not part of the design. I wanted a middle ground: a focused, browser‑only tool where styled text is still just text, with a reversible path back to plain characters.


What It’s Like to Use

You type or paste text into an editor. You pick a style: bold, script, fullwidth, bubble, and so on. The preview updates in a few frames that resemble real uses, a “hero” heading, a feed‑style post, a narrow mobile block. When it feels right, you copy the styled version to your clipboard.

Three properties carry the whole experience. Changes feel undoable. Styled text can be normalized back to something plain. And you can see the text in context before pasting it into anything important.

Underneath there’s a fair amount of logic. On the surface it should feel like a text box that happens to be unusually helpful.


Designing Around the Text

I designed around the text, not the UI chrome. The editor keeps an internal “raw” version, and styled variants are derived from it. Styled text never becomes the main record. Transformers are pure functions from one string to another, each with a rough reverse where possible.

type EditorState = {
  value: string;
  history: string[];
  future: string[];
};

function updateEditor(state: EditorState, nextValue: string): EditorState {
  if (nextValue === state.value) return state; // no-op, avoid noisy history
  return {
    value: nextValue,
    history: [...state.history, state.value],
    future: [], // clear redo stack on new input
  };
}

That state shape buys predictable undo/redo and immutable updates, and it gives me one place to add behaviors without tangling UI and state. The state layer behind the editor tracks value, history, and basic selection. It is deliberately not a full-blown document model.


Unicode as a Style Palette

The core of the app is a small library of transformers that map basic ASCII characters into different Unicode ranges. Each transformer knows which characters it can safely handle and how to map each one to its styled counterpart, or leave it alone.

type Transformer = (input: string) => string;

function mapCharacters(table: Record<string, string>): Transformer {
  return (input) =>
    Array.from(input)
      .map((ch) => table[ch] ?? ch)
      .join("");
}

Each style becomes a lookup table plus a call to mapCharacters. Some ranges are contiguous, which is simple offset math. Others need sparse maps for scattered code points. For lighter styles like strikethrough or underline, I lean on combining marks so the underlying characters survive and copy‑paste keeps working.

Normalization is the other half. Each transformer has a rough inverse that maps its characters back to plain ones, or strips combining marks when that’s the right answer. It isn’t perfect for every Unicode edge case. It reliably turns most styled strings back into something ordinary without re‑typing.


Keeping the Pipeline Honest

The interface disappears quickly because there are only four interactions: editor input, style selection, preview viewing, and copy/normalize. Rather than scattering logic across components, transforms run as a pipeline:

function useTransformPipeline(transformers: Transformer[]) {
  return (text: string) =>
    transformers.reduce((current, transform) => transform(current), text);
}

The editor owns the raw text. The pipeline produces the styled version. The preview listens and renders, with no special cases of its own.

On top of that sits a small design system: shared tokens for color and typography, a handful of components shared by the landing page and the studio, and a theme toggle for light, dark, and system. It’s not elaborate. It does make the tool feel like a product rather than a weekend script I forgot to delete, which, to be fair, is how some of my experiments end.


What I Said No To

I made the tradeoffs explicitly. Readable transformers and a small, explicit style set instead of every obscure Unicode trick. Browser‑only, so everything runs client‑side: better latency, no data leaves the page, simpler deployment. A few familiar layout frames instead of a full template playground.

And a list of things I deliberately didn’t build: collaborative features, rich formatting beyond simple text, arbitrary custom mappings in the UI. Those might be interesting later. They would have pulled the project away from its main job, which is quickly exploring Unicode‑based styles that still behave like plain text.

You could recreate everything it does with a Unicode chart, a terminal, and a heroic amount of patience. I built Unicode Studio so I could spend that patience on design decisions instead.