はじめに
文字数カウントツールは数多く存在しますが、「毎日使うからこそシンプルに」という思想で、必要最小限の機能に特化した文字数チェッカーを開発しました。リリース後にユーザーから文字数カウント方式についてのコメントをいただき、Unicode文字数方式への改善も行いました。YouTube台本制作で毎日文字数チェックを行う中で感じた「余計な機能はいらない」という課題を解決しながら、正確な文字カウントを実現するツールです。
本記事について
今回の開発およびこの記事執筆は、Claude Code(Anthropic社のAI)との協業で進めました。
AI時代の新しい開発スタイルの実践例として参考になれば幸いです。
この記事で分かること
- Unicode文字数方式とUTF-16コードユニット方式の技術的違い
- 異体字セレクタや結合絵文字の正確なカウント方法
- リアルタイム文字カウントのJavaScript実装方法
- 3パターンの文字数カウント(改行含む・改行除く・空白除く)の実装
- プライバシー配慮のクライアントサイド完結型設計
- レスポンシブデザインでの文字カウント表示UI
この記事の対象者
- フロントエンドの文字列処理に興味があるエンジニア
- シンプルなWebアプリケーション開発を学びたい方
- 日常的に文字数カウントツールを使用している方
- JavaScriptでのリアルタイム処理実装に関心がある方
動作環境・使用技術
- 開発環境: Windows 11, Claude Code
- 言語: HTML5, CSS3, JavaScript(ES5準拠)
- 特徴: 外部ライブラリ依存なし、単一HTMLファイル構成
- デバイス対応: レスポンシブデザイン(PC・スマートフォン・タブレット)
自己紹介
ホネグミ代表のなおきちです。エンジニア/マーケターとして、クライアントの「こうしたい」をITと経営の両視点から形にしています。
最近はClaude Codeとの協業開発に注力しており、思想駆動型サービス開発を実践中です。
完成品・デモ
主な機能
- リアルタイム文字カウント: 入力と同時に3パターンの文字数を表示
- 3種類の文字数パターン: 改行含む、改行除く、改行・空白除く
- プライバシー保護: 全てクライアントサイドで処理、データ送信なし
- レスポンシブ対応: PC・モバイル両対応の快適なUI
- 高速動作: 外部ライブラリ依存なしの軽量実装
技術構成
【アーキテクチャ概要】
単一HTMLファイル構成
├── HTML5 Structure
│ ├── semantic markup
│ ├── テキストエリア(textarea)
│ └── 3種類の結果表示エリア
├── CSS3 Styling
│ ├── レスポンシブグリッドレイアウト
│ ├── モバイルファーストデザイン
│ └── グラデーション・ホバー効果
└── JavaScript (ES5)
├── リアルタイム文字カウント処理
├── 正規表現による文字種別処理
└── DOM操作によるリアルタイム表示更新
採用技術とその理由
1. 単一HTMLファイル構成
- 配布・組み込みの簡便性
- 外部依存関係の完全排除
- 高速ローディングの実現
2. ES5準拠JavaScript
- 幅広いブラウザ対応
- 軽量で高速な処理
- 依存ライブラリなしの純粋実装
3. クライアントサイド完結型
- プライバシー保護の徹底
- サーバーコスト削減
- オフライン環境での動作
実装手順
1. HTMLの基本構造
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="文字数チェッカー【無料】でテキストの文字数をリアルタイムカウント!">
<title>文字数チェッカー</title>
</head>
<body>
<div class="character-counter-wrapper">
<div class="character-counter">
<h2>📝 文字数チェッカー</h2>
<p>テキストの文字数をリアルタイムでカウント。入力した文字はインターネットに一切送信されないため、安心してご利用いただけます。</p>
<!-- テキスト入力エリア -->
<div class="input-section">
<label for="textInput">カウントしたいテキストを入力または貼り付けてください</label>
<textarea
id="textInput"
class="text-input"
placeholder="ここにテキストを入力してください..."
oninput="countCharacters()"
></textarea>
</div>
<!-- 結果表示エリア -->
<div class="results">
<div class="result-item">
<span class="result-number" id="totalCount">0</span>
<div class="result-label">文字数<br>(改行含む)</div>
</div>
<div class="result-item">
<span class="result-number" id="noBreakCount">0</span>
<div class="result-label">改行を除いた<br>文字数</div>
</div>
<div class="result-item">
<span class="result-number" id="noSpaceCount">0</span>
<div class="result-label">改行・空白を除いた<br>文字数</div>
</div>
</div>
</div>
</div>
</body>
</html>
2. 核心となる文字カウントロジック(Unicode文字数方式)
function countCharacters() {
// テキストエリアから入力値を取得
const text = document.getElementById('textInput').value;
// Unicode文字数方式でカウント(スプレッド演算子使用)
// 1. 文字数(改行含む)- Unicode文字数でカウント
const totalCount = [...text].length;
// 2. 改行を除いた文字数
// \r?\n で改行文字(CR+LFまたはLF)を除去
const noBreakCount = [...text.replace(/\r?\n/g, '')].length;
// 3. 改行・空白を除いた文字数
// [\r\n\s\t ] で改行・半角空白・タブ・全角空白を除去
const noSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;
// 結果をHTMLに反映
// toLocaleString()で数値を3桁区切りで表示
document.getElementById('totalCount').textContent = totalCount.toLocaleString();
document.getElementById('noBreakCount').textContent = noBreakCount.toLocaleString();
document.getElementById('noSpaceCount').textContent = noSpaceCount.toLocaleString();
}
Unicode文字数方式と従来の方式の比較
// 従来の方式(UTF-16コードユニット数)
const oldMethod = text.length;
// 新しい方式(Unicode文字数)
const newMethod = [...text].length;
// 異体字セレクタ付き文字の例
const testChar = '葛󠄀'; // 葛 + 異体字セレクタ
console.log(testChar.length); // 3(UTF-16コードユニット)
console.log([...testChar].length); // 2(Unicode文字数)
// サロゲートペア文字の例
const surrogatePair = '𩸽'; // 鱼偏の漢字
console.log(surrogatePair.length); // 2(UTF-16コードユニット)
console.log([...surrogatePair].length); // 1(Unicode文字数)
// 結合絵文字の例
const combinedEmoji = '👩👩👦👦'; // 家族絵文字
console.log(combinedEmoji.length); // 11(UTF-16コードユニット)
console.log([...combinedEmoji].length); // 7(Unicode文字数)
3. レスポンシブCSS設計
/* リセット・ベーススタイル */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* メインコンテナ */
.character-counter-wrapper {
font-family: 'Helvetica Neue', Arial, sans-serif !important;
line-height: 1.6 !important;
color: #333 !important;
background: #f5f7fa !important;
padding: 20px !important;
}
/* 中央配置とカード設計 */
.character-counter {
max-width: 800px !important;
margin: 0 auto !important;
background: white !important;
border-radius: 10px !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05) !important;
padding: 20px !important;
}
/* テキストエリアのスタイル */
.text-input {
width: 100%;
min-height: 300px;
padding: 15px;
border: 2px solid #e0e6ed;
border-radius: 10px;
font-size: 16px;
font-family: inherit;
resize: vertical;
transition: all 0.3s ease;
background: #f8f9fa;
}
.text-input:focus {
outline: none;
border-color: #2c5aa0;
background: white;
box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1);
}
/* 結果表示グリッドレイアウト */
.results {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 30px 0;
}
/* 結果カードデザイン */
.result-item {
background: linear-gradient(135deg, #2c5aa0 0%, #4a7bc8 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 12px rgba(44, 90, 160, 0.2);
transition: transform 0.3s ease;
}
.result-item:hover {
transform: translateY(-3px);
}
.result-number {
font-size: 2.2rem;
font-weight: bold;
display: block;
margin-bottom: 8px;
}
/* モバイル対応 */
@media (max-width: 600px) {
.character-counter-wrapper {
padding: 10px !important;
}
.character-counter {
padding: 15px !important;
}
.text-input {
min-height: 200px;
padding: 12px;
}
.results {
grid-template-columns: 1fr;
gap: 10px;
}
.result-number {
font-size: 1.8rem;
}
}
Unicode文字数方式への改善経緯
ユーザーコメントからの気づき
リリース後、Qiitaで以下のようなコメントをいただきました:
葛 1文字、葛󠄀 3文字、𩸽 2文字、👩👩👦👦 11文字という結果になりましたが仕様ですか?
このコメントで、従来のtext.length
方式(UTF-16コードユニット数)では特殊文字が正確にカウントできないことが判明。
他のWebサービスと比較した結果、「1,2,1,7文字」というUnicode文字数方式の結果が一般的であることが確認できました。
改善実装の技術的詳細
// 改善前(UTF-16コードユニット数)
function oldCountCharacters() {
const text = document.getElementById('textInput').value;
const totalCount = text.length; // UTF-16コードユニット数
// 異体字や結合絵文字で不正確な結果
}
// 改善後(Unicode文字数)
function newCountCharacters() {
const text = document.getElementById('textInput').value;
const totalCount = [...text].length; // Unicode文字数
// 異体字やサロゲートペアで正確な結果
}
スプレッド演算子の動作原理:
-
[...text]
は文字列をUnicodeコードポイント単位で配列に分解 - サロゲートペア(2つのUTF-16コードユニット)を1つの文字として扱う
- 異体字セレクタは基本文字と別の文字としてカウント(他のWebサービスと同様)
工夫したポイント・苦労した点
1. Unicode文字数方式への改善実装
課題: 従来のtext.length
では特殊文字の正確なカウントが不可
解決策: スプレッド演算子を使用したUnicode文字数カウント
// 改善前(UTF-16コードユニット数)
const oldTotalCount = text.length;
const oldNoBreakCount = text.replace(/\r?\n/g, '').length;
const oldNoSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length;
// 改善後(Unicode文字数)
const newTotalCount = [...text].length;
const newNoBreakCount = [...text.replace(/\r?\n/g, '')].length;
const newNoSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;
効果: 異体字やサロゲートペアでも他のWebサービスと一致する正確な結果
2. 文字カウントパターンの設計
課題: 用途に応じた適切な文字数計算方法の提供
解決策: 3種類の文字数パターンを同時表示
// パターン1: 改行含む(最も基本的)
const totalCount = [...text].length;
// パターン2: 改行除く(文章の実質文字数)
const noBreakCount = [...text.replace(/\r?\n/g, '')].length;
// パターン3: 改行・空白除く(純粋な文字数)
const noSpaceCount = [...text.replace(/[\r\n\s\t ]/g, '')].length;
効果: YouTube台本、記事執筆、SNS投稿など様々な用途に対応
3. リアルタイム処理のパフォーマンス最適化
課題: 入力のたびに処理が発生するため、重い処理だとUIが重くなる
解決策: 軽量な文字列処理のみで構成
// 重い処理を避け、シンプルな文字列置換のみ使用
function countCharacters() {
const text = document.getElementById('textInput').value;
// 正規表現も最小限に抑制
const totalCount = text.length; // 最高速
const noBreakCount = text.replace(/\r?\n/g, '').length; // 高速
const noSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length; // 高速
// DOM更新も最小限
document.getElementById('totalCount').textContent = totalCount.toLocaleString();
document.getElementById('noBreakCount').textContent = noBreakCount.toLocaleString();
document.getElementById('noSpaceCount').textContent = noSpaceCount.toLocaleString();
}
4. 全角・半角空白の適切な処理
課題: 日本語特有の全角空白( )の処理
解決策: 正規表現で全角・半角空白を包括的に処理
// [\r\n\s\t ] で以下を除去:
// \r\n : 改行文字(Windows/Mac/Linux対応)
// \s : 半角空白
// \t : タブ文字
// : 全角空白(日本語特有)
const noSpaceCount = text.replace(/[\r\n\s\t ]/g, '').length;
5. プライバシー保護の実装
課題: ユーザーの入力データを安全に処理
解決策: 完全クライアントサイド処理
// サーバー送信なし、外部API呼び出しなし
// 全ての処理をブラウザ内で完結
function countCharacters() {
// ローカル処理のみ
const text = document.getElementById('textInput').value;
// ... 文字数計算処理
// ネットワーク通信は一切発生しない
}
6. レスポンシブデザインでの視認性確保
課題: 3つの結果を小画面でも見やすく表示
解決策: CSS Gridによる柔軟レイアウト
.results {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
@media (max-width: 600px) {
.results {
grid-template-columns: 1fr; /* モバイル: 縦並び */
}
}
パフォーマンスの最適化
1. 単一ファイル構成によるローディング最適化
従来の構成(複数ファイル):
├── index.html
├── style.css ← HTTPリクエスト
├── script.js ← HTTPリクエスト
└── (外部ライブラリ) ← HTTPリクエスト
今回の構成(単一ファイル):
└── index.html(CSS・JS内包) ← HTTPリクエスト1回のみ
効果: 初期ローディング時間を大幅短縮
2. ES5準拠による互換性確保
// ES6以降の機能を避けてES5で実装
// const, let → var
// テンプレートリテラル → 文字列結合
// アロー関数 → function記法
function countCharacters() {
var text = document.getElementById('textInput').value;
var totalCount = text.length;
// ... 古いブラウザでも確実に動作
}
3. 数値表示の最適化
// toLocaleString()で3桁区切り表示
// 例: 12345 → 12,345
document.getElementById('totalCount').textContent = totalCount.toLocaleString();
まとめ
シンプルな文字数チェッカーの開発を通じて、以下の知見を得ました:
- Unicode文字数方式の重要性: 特殊文字も正確にカウントするための技術的必須要素
- ユーザーフィードバックの価値: 実用ツールはユーザーの声で進化する
- 機能の厳選の重要性: 毎日使うツールこそシンプル設計が効果的
- リアルタイム処理の最適化: 軽量な文字列処理でスムーズなUX実現
- プライバシーファーストの設計: クライアントサイド完結で安全性確保
技術的にはシンプルながら、Unicode文字数方式で正確な計測を実現し、日常的に使える実用性の高いツールを開発できました。ユーザーからのフィードバックを受けた改善が、より正確で信頼できるツールへと進化させるきっかけとなりました。
Unicode文字数カウント機能の実装や、特殊文字を扱うWebアプリケーション開発に興味がある方の参考になれば幸いです。
実際のサービス
免責事項: 本ツールはシンプルな文字数カウント目的で作成されました。文字数の計算結果は参考値であり、特定のプラットフォームでの実際の文字数制限とは異なる場合があります。