Case Study

Unicode Studio

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

Unicode Studio is a small web app I built for a very specific itch: I like the subtle energy that styled text can add to a line—bold names, softer script headings, bubble text in bios—but I don’t like tools that export images, break copy‑paste, or lock me into a specific platform.

I wanted something quieter: type plain text once, explore a few looks, see how they behave in realistic layouts, and then leave with Unicode characters that behave like normal text almost everywhere.


Why This Exists

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.

Existing generators tended to be either “toy” or “black box.” They rarely showed how the text would look in context, and they didn’t make it easy to get back to a “normal” version.

Unicode Studio is my attempt at a middle ground: a focused, browser‑only tool that treats styled text as still just text, with a reversible path back to plain characters.


What It Feels Like to Use

The experience is intentionally simple:

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

The emphasis is on safety and reversibility:

  • Changes feel undoable.
  • You can normalize styled text back to something plain.
  • You can see the text in context before you paste it into anything important.

Underneath, there’s a fair amount of logic; on the surface, it should feel like “just a text box” that happens to be unusually helpful.


How I Shaped the System

I tried to design around the text, not the UI chrome. Conceptually, the architecture looks like this:

Editor & Style Picker → State & Transform Engine → Previews → Clipboard

A few guiding choices:

  • Text as the single source of truth. I keep an internal “raw” version and derive styled variants from that, instead of letting styled text become the main record.
  • A small state layer behind the editor. Enough to track value, history, and basic selection, but not a full-blown document model.
  • Transformers as composable functions. Each style is a pure function from one string to another, with a rough reverse where possible.

A simplified version of the editor state update looks like this:

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
  };
}

This pattern gives me:

  • predictable undo/redo,
  • immutable updates,
  • a clear place to add behaviors without tangling UI and state.

Using Unicode as a Style Palette

The core of Unicode Studio 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 character to its styled counterpart (or leave it alone).

A conceptual helper looks like this:

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

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

With this, each style becomes just a lookup table plus a call to mapCharacters. Some ranges are contiguous (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 remain intact and copy‑paste‑friendly.

There’s also a simple normalization path: 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, but it’s enough to turn most styled strings back into something ordinary without re‑typing.


Keeping the UI Calm and Predictable

I wanted the interface to disappear quickly, so the core interactions are:

  • editor input,
  • style selection,
  • preview viewing,
  • copy / normalize actions.

Rather than scattering logic across components, I treat transforms as a pipeline:

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

This keeps the mental model simple:

  • The editor owns the “raw” text.
  • The pipeline produces the styled version.
  • The preview just listens and renders, without its own special cases.

On top of that, there’s a small design system: shared tokens for color and typography, a handful of components that both the landing page and studio share, and a theme toggle (light, dark, system). It’s not elaborate, but it makes the tool feel more like a product than a weekend script I forgot to delete—which, to be fair, is how some of my experiments end.


What I Optimized For (and What I Didn’t)

I made a few explicit tradeoffs:

  • Optimized for clarity over cleverness. I favored readable transformers and a small, explicit style set instead of trying to support every obscure Unicode trick.
  • Optimized for browser‑only. Everything runs client‑side: better latency, no data leaves the page, simpler deployment.
  • Optimized for realistic previews. I chose a few familiar layout frames rather than a full template playground.

I deliberately did not:

  • add collaborative features,
  • build rich formatting beyond simple text,
  • support arbitrary custom mappings in the UI.

Those might be interesting future directions, but they would have pulled the project away from its main goal: quickly exploring Unicode‑based styles that still behave like plain text.


What I Learned

Building Unicode Studio reinforced a few ideas for me:

  • Treating plain text as the primary source of truth keeps the entire system easier to reason about.
  • A small library of composable, well‑named transformers is more maintainable than a long chain of ad‑hoc replacements.
  • For tools like this, predictability beats novelty; users (including future‑me) care more that copy‑paste always works than that the app knows every possible decorative character.

You could absolutely recreate everything it does with a Unicode chart, a terminal, and a heroic amount of patience. But that’s exactly the kind of patience I prefer to spend on design and architecture decisions instead—so I built Unicode Studio to do the tedious part once, properly, and keep the actual experience simple.