Skip to main content

Command Palette

Search for a command to run...

Building a Lorem Ipsum Generator in React — No Library, Custom Word Pool, and Sentence Variation

How we built a Lorem Ipsum generator using a static word pool, randomized sentence and paragraph construction, and a three-mode toggle — no external library, under 160 lines.

Updated
5 min read
Building a Lorem Ipsum Generator in React — No Library, Custom Word Pool, and Sentence Variation

A Lorem Ipsum generator has one job: produce readable-looking placeholder text on demand. The interesting constraint is doing it without a library — just a word pool, some arithmetic, and composable generation functions.

Here's how we built the Lorem Ipsum Generator at Ultimate Tools.


The Word Pool

The generator uses the classic Lorem Ipsum vocabulary — 69 Latin-derived words from Cicero's De Finibus:

const WORDS = [
    "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
    "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
    "magna", "aliqua", "ut", "enim", "ad", "minim", "veniam", "quis", "nostrud",
    "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea",
    "commodo", "consequat", "duis", "aute", "irure", "dolor", "in", "reprehenderit",
    "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla",
    "pariatur", "excepteur", "sint", "occaecat", "cupidatat", "non", "proident",
    "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id",
    "est", "laborum"
];

This is intentionally the standard Latin pool rather than custom English filler. The words look like text without being readable, which is exactly why the convention has persisted since the 1500s — reviewers focus on layout, not content.


State Shape

The component manages three values:

const [count, setCount] = useState(3);
const [type, setType] = useState<"paragraphs" | "sentences" | "words">("paragraphs");
const [text, setText] = useState("");
  • count — how many units to generate (1–100)

  • type — the output granularity

  • text — the read-only output string

A fourth piece of state, copied, drives the clipboard button icon but isn't involved in generation.


The Generation Functions

Three nested functions build output from the smallest unit up:

const generateSentence = () => {
    const length = Math.floor(Math.random() * 10) + 5;  // 5–14 words
    let sentence = "";
    for (let i = 0; i < length; i++) {
        const word = WORDS[Math.floor(Math.random() * WORDS.length)];
        sentence += (i === 0
            ? word.charAt(0).toUpperCase() + word.slice(1)
            : word)
            + (i === length - 1 ? "." : " ");
    }
    return sentence;
};

Sentences are 5–14 words. The first word is capitalized (charAt(0).toUpperCase() + word.slice(1)). The last word gets a period, everything else gets a space. A single loop handles start, middle, and end with index checks rather than special-casing each position separately.

const generateParagraph = () => {
    const length = Math.floor(Math.random() * 5) + 3;  // 3–7 sentences
    let paragraph = "";
    for (let i = 0; i < length; i++) {
        paragraph += generateSentence() + " ";
    }
    return paragraph.trim();
};

Paragraphs are 3–7 sentences joined with spaces. The trailing space after the last sentence is trimmed. Each sentence ends with a period, so sentences read as distinct.


The Main Generate Function

const generate = () => {
    let result = "";

    if (type === "words") {
        for (let i = 0; i < count; i++) {
            result += WORDS[Math.floor(Math.random() * WORDS.length)] + " ";
        }
    } else if (type === "sentences") {
        for (let i = 0; i < count; i++) {
            result += generateSentence() + " ";
        }
    } else {
        for (let i = 0; i < count; i++) {
            result += generateParagraph() + "\n\n";
        }
    }

    setText(result.trim());
    setCopied(false);
};

All three modes produce a single string. The paragraph mode separates blocks with \n\n. Trimming the final result removes trailing whitespace or newlines.

setCopied(false) resets the clipboard button icon on each new generation — if you copy, then generate again, the button reverts to the copy icon to signal that the new text isn't copied yet.


Auto-Generate on Load

The component generates on mount:

if (!text) generate();

This runs in the render body — if text is empty (the initial state), generate() fires immediately. On subsequent renders, text is non-empty so the condition is false. The user sees content immediately without pressing a button.

This is a pattern worth noting: it avoids a useEffect for one-time initialization when the function has no side effects that need cleanup. generate only writes to state and produces no subscriptions or DOM mutations.


The Three-Mode Toggle

{["paragraphs", "sentences", "words"].map((t) => (
    <button
        key={t}
        onClick={() => setType(t as typeof type)}
        className={cn(
            "flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors",
            type === t
                ? "bg-zinc-900 text-white dark:bg-zinc-50 dark:text-zinc-900"
                : "bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400"
        )}
    >
        {t}
    </button>
))}

The active mode gets a filled background (zinc-900/zinc-50 for dark/light). Clicking a mode changes type but doesn't re-generate — the user triggers generation explicitly with the Generate button. This avoids regenerating on every toggle when the user might be adjusting both type and count before generating.


Read-Only Textarea

The output renders in a readOnly textarea rather than a <div> or <pre>:

<textarea
    readOnly
    value={text}
    className="h-96 w-full resize-none rounded-xl font-mono text-sm leading-relaxed ..."
/>

A textarea preserves whitespace (the \n\n between paragraphs) and lets users select and copy text with the keyboard (Ctrl+A, Ctrl+C) without relying on the clipboard button. readOnly prevents editing but keeps the selection behavior of a text input.

font-mono on the output makes character widths consistent, which is useful when pasting into fixed-width environments. resize-none removes the browser's default resize handle — the height is fixed at h-96 (384px).


Copy to Clipboard

const copyToClipboard = () => {
    navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
};

Standard async clipboard write. The copied state toggles the button icon from <Copy> to <Check> for 2 seconds — the same pattern used across all copy-enabled tools in the suite.


Try It

The generator is live at Lorem Ipsum Generator — paragraphs, sentences, or words, set the count, copy. Part of Ultimate Tools, a free collection of browser-based developer and productivity tools.

More from this blog

U

Ultimate Tools — Developer Blog

129 posts