This is a solution to the Character counter challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.
Users should be able to:
- Analyze the character, word, and sentence counts for their text in real-time
- Exclude / include spaces in their character count
- Set a character limit and receive a warning message if their text exceeds the limit
- See the approximate reading time of their text
- Analyze the letter density of their text (sorted, with "See more / See less")
- Select their color theme (light / dark, persisted between visits)
- View an optimized layout for mobile, tablet, and desktop
- See hover and focus states for all interactive elements
- Solution URL: TBD
- Live Site URL: TBD
- Semantic HTML5 (
header,main,section,article) - CSS custom properties (design-token architecture, two-theme mapping)
- Flexbox + CSS Grid
- Mobile-first responsive design with progressive enhancement
- Vanilla JavaScript (ES modules, no framework)
- Vite — dev server and build tool
- DM Sans variable font (self-hosted)
src/
├── css/
│ ├── main.css # entry, imports all others
│ ├── fonts.css # DM Sans @font-face
│ ├── tokens.css # colors, spacing, radii, typography
│ ├── theme.css # dark (default) and light mappings
│ ├── base.css # reset, body, focus-visible, reduced-motion
│ ├── utilities.css # visually-hidden helper
│ ├── layout.css # page / section layout (mobile-first)
│ ├── components.css # brand, theme-toggle, textarea, stats, density
│ └── responsive.css # tablet (≥640px) and desktop (≥1024px) overrides
└── js/
├── main.js # DOM wiring
├── analyzer.js # pure: counts, reading time, letter density
└── theme.js # theme toggle with localStorage persistence
- Centralizing every spacing, color, and typography value as a CSS custom property from the start made theme switching a one-class toggle (
data-themeon:root) with no component rewrites. prefers-color-scheme+localStoragegives a robust first-load theme: respect the OS preference, let the user override, remember the choice.- The
\p{L}Unicode regex flag is a clean way to count letters across scripts without hardcoding A–Z. - Mobile-first beats desktop-first when the design has clear responsive tiers: overrides add behavior, never undo it.
export function computeLetterDensity(text) {
const letters = text.toUpperCase().match(/\p{L}/gu) ?? [];
const total = letters.length;
const counts = new Map();
for (const ch of letters) counts.set(ch, (counts.get(ch) ?? 0) + 1);
return {
total,
entries: [...counts.entries()]
.map(([letter, count]) => ({ letter, count, percent: (count / total) * 100 }))
.sort((a, b) => b.count - a.count || a.letter.localeCompare(b.letter)),
};
}- Explore
<output>andaria-live="polite"combinations to find the screen-reader-friendliest way to announce real-time changing counts without over-announcing each keystroke. - Container queries for the stat cards, so each card could adapt to its own width rather than the viewport.
- Claude Code (Anthropic) — used for planning the phased implementation, extracting design tokens from the Figma file via the Figma MCP server, generating boilerplate for the analyzer module, and automating screenshot capture with Playwright.
- What worked: token extraction from Figma MCP gave exact values rather than eyeballing; phased checklist kept each commit focused.
- What didn't: initial metadata fetches hit rate limits; had to narrow requests by specific node IDs.

