Text Expander & Snippets

Expand abbreviations with built-in and custom dictionaries. Create and insert reusable text snippets.

Text
100% Free
Runs in Browser

Preview

Source Code

Toggle the controls below the code to live-edit the initial state values.

text-expander.tsx
import { useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Copy, Check, Plus, Trash2 } from "lucide-react";

const DEFAULT_ABBREVIATIONS: Record<string, string> = {
    "btw": "by the way", "fyi": "for your information", "asap": "as soon as possible",
    "imo": "in my opinion", "tbh": "to be honest", "afaik": "as far as I know",
    "brb": "be right back", "eta": "estimated time of arrival", "diy": "do it yourself",
    "faq": "frequently asked questions", "rsvp": "please respond", "tbd": "to be determined",
    "aka": "also known as", "eg": "for example", "ie": "that is", "etc": "et cetera",
    "vs": "versus", "dept": "department", "govt": "government", "approx": "approximately",
};

export default function TextExpander() {
    const [input, setInput] = useState("");
    const [output, setOutput] = useState("");
    const [copied, setCopied] = useState(false);
    const [customDict, setCustomDict] = useState<Record<string, string>>({});
    const [newKey, setNewKey] = useState("");
    const [newValue, setNewValue] = useState("");
    const [snippets, setSnippets] = useState<{ name: string; content: string }[]>([
        { name: "greeting", content: "Dear Sir/Madam,\n\nI hope this email finds you well." },
        { name: "closing", content: "Thank you for your time and consideration.\n\nBest regards," },
    ]);
    const [newSnippetName, setNewSnippetName] = useState("");
    const [newSnippetContent, setNewSnippetContent] = useState("");

    const allDict = { ...DEFAULT_ABBREVIATIONS, ...customDict };

    const expand = () => {
        let result = input;
        for (const [abbr, expansion] of Object.entries(allDict)) {
            result = result.replace(new RegExp(`\\b${abbr}\\b`, "gi"), expansion);
        }
        setOutput(result);
    };

    const addCustom = () => {
        if (newKey && newValue) {
            setCustomDict((prev) => ({ ...prev, [newKey.toLowerCase()]: newValue }));
            setNewKey("");
            setNewValue("");
        }
    };

    const addSnippet = () => {
        if (newSnippetName && newSnippetContent) {
            setSnippets((prev) => [...prev, { name: newSnippetName, content: newSnippetContent }]);
            setNewSnippetName("");
            setNewSnippetContent("");
        }
    };

    const insertSnippet = (content: string) => {
        setInput((prev) => prev + (prev ? "\n" : "") + content);
    };

    const copyOutput = async () => {
        await navigator.clipboard.writeText(output);
        setCopied(true);
        setTimeout(() => setCopied(false), 1500);
    };

    return (
        <div className="space-y-4">
            <Textarea placeholder="Type text with abbreviations (e.g., 'btw, fyi, asap')..." value={input}
                onChange={(e) => setInput(e.target.value)} className="min-h-[120px] resize-y text-sm" />
            <Button onClick={expand}>Expand Abbreviations</Button>

            {output && (
                <div className="relative rounded-lg border bg-muted/50 p-4">
                    <pre className="whitespace-pre-wrap text-sm">{output}</pre>
                    <Button variant="ghost" size="icon" className="absolute top-2 right-2" onClick={copyOutput}>
                        {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
                    </Button>
                </div>
            )}

            {/* Custom Dictionary */}
            <div className="rounded-lg border p-4 space-y-3">
                <span className="text-sm font-medium">Custom Dictionary</span>
                <div className="flex gap-2">
                    <Input placeholder="abbr" value={newKey} onChange={(e) => setNewKey(e.target.value)} className="w-24" />
                    <Input placeholder="expansion" value={newValue} onChange={(e) => setNewValue(e.target.value)} className="flex-1" />
                    <Button size="sm" onClick={addCustom}><Plus className="h-3 w-3" /></Button>
                </div>
                <div className="flex flex-wrap gap-2">
                    {Object.entries(customDict).map(([k, v]) => (
                        <Badge key={k} variant="outline" className="text-xs">
                            {k} → {v}
                            <button className="ml-1 text-muted-foreground hover:text-foreground"
                                onClick={() => setCustomDict((prev) => { const n = { ...prev }; delete n[k]; return n; })}>×</button>
                        </Badge>
                    ))}
                </div>
                <p className="text-xs text-muted-foreground">
                    Built-in: {Object.keys(DEFAULT_ABBREVIATIONS).length} abbreviations • Custom: {Object.keys(customDict).length}
                </p>
            </div>

            {/* Snippets */}
            <div className="rounded-lg border p-4 space-y-3">
                <span className="text-sm font-medium">Snippets</span>
                <div className="flex flex-wrap gap-2">
                    {snippets.map((s, i) => (
                        <Button key={i} variant="outline" size="sm" onClick={() => insertSnippet(s.content)}>
                            {s.name}
                        </Button>
                    ))}
                </div>
                <div className="flex gap-2">
                    <Input placeholder="name" value={newSnippetName} onChange={(e) => setNewSnippetName(e.target.value)} className="w-24" />
                    <Input placeholder="content" value={newSnippetContent} onChange={(e) => setNewSnippetContent(e.target.value)} className="flex-1" />
                    <Button size="sm" onClick={addSnippet}><Plus className="h-3 w-3" /></Button>
                </div>
            </div>
        </div>
    );
}
Props Playground
5 props

Required Libraries

Install the external dependencies used by this tool.

bun add lucide-react react

Shadcn UI Setup

Add the required Shadcn UI primitives to your project.

bun x --bun shadcn@latest add badge button input textarea

Shadcn UI Components

The following primitives are required in your @/components/ui directory.

Component Import Path
badge @/components/ui/badge
button @/components/ui/button
input @/components/ui/input
textarea @/components/ui/textarea

Imports

All import statements used in this tool.

Source Exports
react useState
@/components/ui/textarea Textarea
@/components/ui/input Input
@/components/ui/button Button
@/components/ui/badge Badge
lucide-react Copy, Check, Plus, Trash2

State Management

React state variables managed within this tool.

Variable Initial Value
input ""
output ""
copied false
newKey ""
newValue ""
snippets [ { name: "greeting", content: "Dear Sir/Madam,\n\nI hope this email finds you well." }, { name: "closing", content: "Thank you for your time and consideration.\n\nBest regards," }, ]
newSnippetName ""
newSnippetContent ""

Variables & Constants

Constants and computed values defined in this tool.

Name Value
allDict { ...DEFAULT_ABBREVIATIONS, ...customDict }

Functional Logic

Internal functions that handle the tool's core logic.

Function Parameters Async
expand() None No
addCustom() None No
addSnippet() None No
insertSnippet() content: string No
copyOutput() None
Yes

External Resources

Documentation, tutorials, and package details for libraries used in this tool.