UI Components Easy
Word Counter
A real-time word and character counter with added features like reading time estimation and sentence counting.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--word-primary: #10b981;
--word-primary-glow: rgba(16, 185, 129, 0.2);
--word-bg: rgba(255, 255, 255, 0.04);
--word-border: rgba(255, 255, 255, 0.08);
--word-text: #f8fafc;
--word-muted: #94a3b8;
--word-stat-bg: rgba(16, 185, 129, 0.08);
--word-stat-border: rgba(16, 185, 129, 0.15);
--word-textarea-bg: rgba(255, 255, 255, 0.04);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: #0b1221;
padding: 1rem;
}
.word-widget {
background: var(--word-bg);
border: 1px solid var(--word-border);
border-radius: 24px;
padding: 1.5rem;
max-width: 600px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
backdrop-filter: blur(16px);
}
.widget-controls {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.btn-text {
background: transparent;
border: 1px solid var(--word-border);
color: var(--word-primary);
font-weight: 600;
cursor: pointer;
font-size: 0.813rem;
padding: 0.3rem 0.75rem;
border-radius: 8px;
transition: all 0.2s;
}
.btn-text:hover {
background: var(--word-stat-bg);
border-color: var(--word-primary);
}
.text-area {
width: 100%;
height: 200px;
padding: 1.25rem;
border: 1px solid var(--word-border);
border-radius: 16px;
background: var(--word-textarea-bg);
color: var(--word-text);
font-family: inherit;
font-size: 0.9375rem;
line-height: 1.6;
resize: vertical;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
margin-bottom: 1.5rem;
}
.text-area::placeholder {
color: var(--word-muted);
}
.text-area:focus {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 0 0 3px var(--word-primary-glow);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
.stat-item {
background: var(--word-stat-bg);
border: 1px solid var(--word-stat-border);
padding: 1rem;
border-radius: 14px;
text-align: center;
}
.stat-value {
display: block;
font-size: 1.375rem;
font-weight: 800;
color: var(--word-text);
font-variant-numeric: tabular-nums;
}
.stat-label {
display: block;
font-size: 0.688rem;
color: var(--word-primary);
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.07em;
margin-top: 0.25rem;
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}const textArea = document.getElementById("text-input");
const wordCountEl = document.getElementById("word-count");
const charCountEl = document.getElementById("char-count");
const sentenceCountEl = document.getElementById("sentence-count");
const readingTimeEl = document.getElementById("reading-time");
const clearBtn = document.getElementById("clear-text");
const copyBtn = document.getElementById("copy-text");
function updateStats() {
const text = textArea.value.trim();
// Word Count
const words = text ? text.split(/\s+/).length : 0;
wordCountEl.textContent = words;
// Char Count
charCountEl.textContent = textArea.value.length;
// Sentence Count
const sentences = text ? text.split(/[.!?]+/).length - 1 : 0;
sentenceCountEl.textContent = Math.max(0, sentences);
// Reading Time (Avg 200 words per minute)
const readingTime = Math.ceil(words / 200);
readingTimeEl.textContent = `~${readingTime}m`;
}
textArea.addEventListener("input", updateStats);
clearBtn.addEventListener("click", () => {
textArea.value = "";
updateStats();
});
copyBtn.addEventListener("click", () => {
textArea.select();
document.execCommand("copy");
const originalText = copyBtn.textContent;
copyBtn.textContent = "Copied!";
setTimeout(() => {
copyBtn.textContent = originalText;
}, 2000);
});
// Init
updateStats();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Word Counter</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="word-widget">
<div class="widget-controls">
<button id="clear-text" class="btn-text">Clear</button>
<button id="copy-text" class="btn-text">Copy</button>
</div>
<textarea id="text-input" placeholder="Start typing or paste your content here..." class="text-area"></textarea>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="word-count">0</span>
<span class="stat-label">Words</span>
</div>
<div class="stat-item">
<span class="stat-value" id="char-count">0</span>
<span class="stat-label">Chars</span>
</div>
<div class="stat-item">
<span class="stat-value" id="sentence-count">0</span>
<span class="stat-label">Sentences</span>
</div>
<div class="stat-item">
<span class="stat-value" id="reading-time">~0m</span>
<span class="stat-label">Reading</span>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useMemo } from "react";
export default function WordCounterRC() {
const [text, setText] = useState("");
const stats = useMemo(() => {
const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
const chars = text.length;
const charsNoSpace = text.replace(/\s/g, "").length;
const sentences = text.trim() === "" ? 0 : text.split(/[.!?]+/).filter(Boolean).length;
const paragraphs = text.trim() === "" ? 0 : text.split(/\n+/).filter((p) => p.trim()).length;
const readingMin = Math.ceil(words / 200);
return { words, chars, charsNoSpace, sentences, paragraphs, readingMin };
}, [text]);
const items = [
{ label: "Words", value: stats.words },
{ label: "Characters", value: stats.chars },
{ label: "Chars (no spaces)", value: stats.charsNoSpace },
{ label: "Sentences", value: stats.sentences },
{ label: "Paragraphs", value: stats.paragraphs },
{ label: "Reading time", value: `${stats.readingMin} min` },
];
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-full max-w-xl">
<h2 className="text-[#e6edf3] font-bold text-xl mb-4">Word Counter</h2>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Start typing or paste your text here…"
rows={8}
className="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] mb-4"
/>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{items.map(({ label, value }) => (
<div key={label} className="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3">
<p className="text-[24px] font-bold text-[#58a6ff] tabular-nums leading-none mb-1">
{value}
</p>
<p className="text-[11px] text-[#8b949e] uppercase tracking-wider">{label}</p>
</div>
))}
</div>
{text.length > 0 && (
<button
onClick={() => setText("")}
className="mt-4 text-sm text-[#8b949e] hover:text-[#f85149] transition-colors"
>
Clear text
</button>
)}
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const text = ref("");
const stats = computed(() => {
const t = text.value;
const words = t.trim() === "" ? 0 : t.trim().split(/\s+/).length;
const chars = t.length;
const charsNoSpace = t.replace(/\s/g, "").length;
const sentences = t.trim() === "" ? 0 : t.split(/[.!?]+/).filter(Boolean).length;
const paragraphs = t.trim() === "" ? 0 : t.split(/\n+/).filter((p) => p.trim()).length;
const readingMin = Math.ceil(words / 200);
return { words, chars, charsNoSpace, sentences, paragraphs, readingMin };
});
const items = computed(() => [
{ label: "Words", value: stats.value.words },
{ label: "Characters", value: stats.value.chars },
{ label: "Chars (no spaces)", value: stats.value.charsNoSpace },
{ label: "Sentences", value: stats.value.sentences },
{ label: "Paragraphs", value: stats.value.paragraphs },
{ label: "Reading time", value: `${stats.value.readingMin} min` },
]);
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-xl">
<h2 class="text-[#e6edf3] font-bold text-xl mb-4">Word Counter</h2>
<textarea
v-model="text"
placeholder="Start typing or paste your text here..."
rows="8"
class="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] mb-4"
/>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div
v-for="item in items"
:key="item.label"
class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3"
>
<p class="text-[24px] font-bold text-[#58a6ff] tabular-nums leading-none mb-1">{{ item.value }}</p>
<p class="text-[11px] text-[#8b949e] uppercase tracking-wider">{{ item.label }}</p>
</div>
</div>
<button
v-if="text.length > 0"
@click="text = ''"
class="mt-4 text-sm text-[#8b949e] hover:text-[#f85149] transition-colors"
>
Clear text
</button>
</div>
</div>
</template><script>
let text = "";
$: words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
$: chars = text.length;
$: charsNoSpace = text.replace(/\s/g, "").length;
$: sentences = text.trim() === "" ? 0 : text.split(/[.!?]+/).filter(Boolean).length;
$: paragraphs = text.trim() === "" ? 0 : text.split(/\n+/).filter((p) => p.trim()).length;
$: readingMin = Math.ceil(words / 200);
$: items = [
{ label: "Words", value: words },
{ label: "Characters", value: chars },
{ label: "Chars (no spaces)", value: charsNoSpace },
{ label: "Sentences", value: sentences },
{ label: "Paragraphs", value: paragraphs },
{ label: "Reading time", value: `${readingMin} min` },
];
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-xl">
<h2 class="text-[#e6edf3] font-bold text-xl mb-4">Word Counter</h2>
<textarea
bind:value={text}
placeholder="Start typing or paste your text here..."
rows="8"
class="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] mb-4"
/>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
{#each items as { label, value }}
<div class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3">
<p class="text-[24px] font-bold text-[#58a6ff] tabular-nums leading-none mb-1">{value}</p>
<p class="text-[11px] text-[#8b949e] uppercase tracking-wider">{label}</p>
</div>
{/each}
</div>
{#if text.length > 0}
<button
on:click={() => (text = "")}
class="mt-4 text-sm text-[#8b949e] hover:text-[#f85149] transition-colors"
>
Clear text
</button>
{/if}
</div>
</div>Word Counter
A fast and efficient word counting tool for writers and editors. Get instant feedback on your text metrics as you type.
Features
- Real-time word and character counting
- Sentence and paragraph counting
- Estimated reading time calculation
- Estimated speaking time calculation
- Clear/Copy text functionality