GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

html css エディタ UI 9選 — コードエディタ 作り方の基本

html css エディタ とは、Web ページの中に置いてコードを打つと色が付き、行番号が並び、括弧が自分で閉じる入力欄のことです。9つともライブラリなし、textarea だけで組んであり、下のグリッドが今すぐ動きます。どの部品もそのままコピーしてページに置けます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並びは人気順ではなく、入力欄ひとつがエディタへ育っていく順です。まず文字に色が乗り(01)、その横に行番号が付き(02)、打っている間に括弧が追いかけ(03)、掴んだ行をまとめて押せるようになり(04)、文書が長くなると全体の地図が要り(05)、探して置き換える場面が来て(06)、打ちかけの語を完成してもらい(07)、間違いに下線が引かれ(08)、最後に目に優しい色へ着替えます(09)。9つすべてで本当の入力は <textarea> が受け持ち、色はその下に敷いた <pre> が描きます。01〜04 は入力そのものを助ける面、05〜07 は長い文書を読むための面、08・09 は間違いと目の負担への対処という、3つのまとまりでも読めます。9つすべてを480×300px の枠で測り、電話の幅でもはみ出しは0件でした。この流れは、コードエディタ 作り方 を段階ごとに進めるときの地図にもなります。同じembed柱では、入力ではなくコマンドの出力が主役のターミナル風 UI 9選と、コードの代わりに言語パックの JSON を検査する多言語サイト 作り方 — 言語パックJSONの検証チェック9選が隣に並んでいます。

01リアルタイム構文ハイライト

文字を透明にした textarea の下に同じ等幅の pre を重ね、入力のたびに正規表現4つで色を塗り直す、ドキュメントサイトのコード入力欄です。

リアルタイム構文ハイライト
新しいタブで開く

01 · リアルタイム構文ハイライト — 計測 累積面積 25.074% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

-webkit-text-fill-colorwhite-space: prescrollTop

規則4つの順序がそのまま優先順位

// トークナイザーの規則は4つ。前の規則が先に勝つ — コメント内の色コードを色として拾わないための順序
var RULES = [
  { cls: 'c', re: /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g },
  { cls: 's', re: /"[^"\n]*"|'[^'\n]*'/g },
  { cls: 'n', re: /#[0-9a-fA-F]{3,8}\b|\b\d+(?:\.\d+)?(?:px|ms|s|%|em|rem|fr)?\b/g },
  { cls: 'k', re: /--[a-zA-Z][\w-]*|\.[a-zA-Z][\w-]*|@[a-z-]+|\b[a-z-]+(?=\s*:)/g }
];

先に拾われた位置は後の規則が触れないので、コメントの中に色コードを書いても色としてではなくコメントひとまとまりとして塗られます。

二層がずれないための共有値

%layer {
  box-sizing: border-box; margin: 0; padding: $sp-2;
  border: 0; border-radius: $r-sm;
  font-family: $mono; font-weight: 500; font-size: $fs; line-height: $lh;
  letter-spacing: 0; tab-size: 2;
  white-space: pre; overflow: auto; overscroll-behavior: contain;
}

フォント・サイズ・行間・パディング・枠線・字間・タブ幅・改行規則を1つのプレースホルダーにまとめて両要素が共有するので、Playwright で計算値8項目を比べても差は0件で、textarea を120px スクロールすれば下の pre も120px 動きます。

02行番号ガターと現在行

左のガターの番号が行数に合わせて増え減り、キャレットのある行だけ背景が点く、エラー行を探す編集画面の部品です。

行番号ガターと現在行
新しいタブで開く

02 · 行番号ガターと現在行 — 計測 累積面積 24.353% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

counter-resetselectionStarttranslateY

番号は CSS カウンターが振る

// 行数が変わるたびガターを描き直す — 番号づけ自体は scss の counter-reset カウンターが担う
function render() {
  var lines = ta.value.split('\n');
  var nums = '';
  lines.forEach(function () { nums += '<span class="gt__num"></span>'; });
  gtrack.innerHTML = nums + '<span class="gt__gcur"></span>';
  gcurEl = gtrack.querySelector('.gt__gcur');
  hl.innerHTML = lines.map(function (l) {
    return '<span class="gt__ln">' + (esc(l) || ' ') + '</span>';
  }).join('');
  countEl.textContent = LBL_COUNT.replace('{n}', lines.length);
}

行数分の空いた <span> を並べるだけで数字は CSS 側のカウンターが振り、6行だった番号に2行打ち足すと8個に増えました。

現在行は selectionStart で数える

// selectionStart までの文字で何行目かを計算し、2つの帯を同じ値で動かす
function update() {
  var idx = ta.value.slice(0, ta.selectionStart).split('\n').length - 1;
  var y = idx * lineH();
  curEl.style.transform = 'translateY(' + y + 'px)';
  if (gcurEl) gcurEl.style.transform = 'translateY(' + y + 'px)';
  sync();
}

ガターの帯も本文の帯も同じ translateY 値を受けるので、本文を40px スクロールするとどちらも -40px ずつ動きます。

03括弧・引用符の自動閉じ

開き括弧を打つと閉じ括弧も一緒に入り、キャレットはその間に残る、短いスニペットを素早く打つ欄です。

括弧・引用符の自動閉じ
新しいタブで開く

03 · 括弧・引用符の自動閉じ — 計測 累積面積 7.31% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setSelectionRangepreventDefaultkey

ペア挿入・素通り・ペア削除の3分岐

var over = s === t && v.charAt(s) === k && (PAIRS[k] === k || ')}]'.indexOf(k) >= 0);
if (PAIRS[k] && !over) {
  // 開きペア — 閉じペアも一緒に入れ、setSelectionRange でキャレットはその間に残す
  e.preventDefault();
  var inner = v.slice(s, t);
  ta.setRangeText(k + inner + PAIRS[k], s, t, 'end');
  ta.setSelectionRange(s + 1, s + 1 + inner.length);
  setState('ペア挿入');
} else if (over) {
  // すでに閉じている位置 — 挿入せずキャレットだけが進む
  e.preventDefault();
  ta.setSelectionRange(s + 1, s + 1);
  setState('素通り');
}

fade( まで打つと値は fade() になってキャレットは5文字目に置かれ、そこでもう一度 ) を打っても値は変わらずキャレットだけが6へ進みます。a{} の間で Backspace を押すと2文字が一緒に消えて a だけが残ります。

04Tab インデントと解除

Tab は2文字を入れ、Shift+Tab は2文字を取る、インデントが意味を持つ設定ファイル編集の部品です。

Tab インデントと解除
新しいタブで開く

04 · Tab インデントと解除 — 計測 累積面積 16.572% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

selectionEndsplittab-size

掴んだ行全体を一度に押す

var lines = v.slice(ls, ne).split(NL);
var firstShift = 0, lastShift = 0;
var moved = lines.map(function (ln, i) {
  if (dir > 0) {
    if (i === 0) firstShift = INDENT.length;
    if (i === lines.length - 1) lastShift = INDENT.length;
    return INDENT + ln;
  }
  var lead = ln.match(/^ {1,2}/);
  var cut = lead ? lead[0].length : 0;
  if (i === 0) firstShift = -cut;
  if (i === lines.length - 1) lastShift = -cut;
  return ln.slice(cut);
});
ta.value = v.slice(0, ls) + moved.join(NL) + v.slice(ne);
// 選択範囲の再計算: 始点は最初の行のずれ分、終点は最後の行のずれ分に従う
ta.setSelectionRange(Math.max(ls, s + firstShift), Math.max(ls + 1, e + lastShift));

3〜5行目(文字位置24〜62)を選んで Tab を押すと各行頭の空白が 0,0,2,2,2 になって選択は26〜64へ移り、Shift+Tab で 0,0,0,0,0 と24〜62へ正確に戻りました。削れる空白がない状態でさらに Shift+Tab を押しても値も選択もそのままです。

05キャンバス ミニマップ

右の細いキャンバスが文字の代わりに行の長さをピクセルの帯で描く、長いファイルで位置をつかむ編集画面の部品です。

キャンバス ミニマップ
新しいタブで開く

05 · キャンバス ミニマップ — 計測 累積面積 15.017% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectgetContextscrollHeight

文字を描かず長さだけを描く

var pitch = h / LINES.length;
var barH = Math.max(1.5, pitch - 1.5);
LINES.forEach(function (ln, i) {
  var bw = Math.max(2, (ln.trim().length / maxLen) * (w - 6));
  ctx.fillStyle = ln.indexOf('//') === 0 ? 'rgba(190,183,173,.40)' : 'rgba(255,247,230,.78)';
  ctx.fillRect(3, i * pitch, bw, barH);
});

30行の文書を幅48px・高さ160px の中に帯だけで描き、描かれたピクセルは2,452個でした。ビューポートの枠を一番下までドラッグすると本文の scrollTop が 0 から 376 までの全域を動き、逆に本文を120px スクロールすると枠は 35.7px 下がって比率が揃います。

06検索・置換バー

Ctrl+F でバーが降りてきて、一致する位置に黄色い帯が付く、長い設定を一気に直す画面の部品です。

検索・置換バー
新しいタブで開く

06 · 検索・置換バー — 計測 累積面積 15.163% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

matchAllmarkscrollIntoView

一致位置を一度に拾って番号を振る

// matchAll で一致位置を一度にすべて拾い、何番目か数えられるよう開始・終了インデックスを残す
function hits() {
  if (!q.value) return [];
  var re = new RegExp(escRe(q.value), 'g');
  return Array.from(ta.value.matchAll(re)).map(function (m) {
    return { s: m.index, e: m.index + m[0].length };
  });
}

4px を打つと帯が6つ敷かれてカウンターは 1/6、Enter で 2/6 へ進みます。置き換えを1回押すと今の位置だけが 8px になって 4px は5つに減り、カウンターは 2/5 と数え直されます。

07キャレット位置の入力補完

打ちかけの語で候補が絞られ、ポップアップがキャレットの真下に開く、決まったキーワードを打つクエリ欄の部品です。

キャレット位置の入力補完
新しいタブで開く

07 · キャレット位置の入力補完 — 計測 累積面積 8.792% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

role="listbox"aria-activedescendantstartsWith

キャレットの x 座標はミラー要素で測る

function caretX(pos) {
  mirror.textContent = ta.value.slice(0, pos);
  var pad = parseFloat(getComputedStyle(ta).paddingLeft) || 0;
  return pad + mirror.offsetWidth - ta.scrollLeft;
}
// ポップアップの左端をキャレットの x に合わせる — 右端を超えるときは内側へ折る
function place(pos) {
  var x = caretX(pos);
  var maxX = ta.clientWidth - pop.offsetWidth - 4;
  pop.style.left = Math.max(0, Math.min(x, Math.max(0, maxX))) + 'px';
}

同じフォントでキャレットより前だけを写した隠し要素の幅を読むので、document.quer まで打ったときポップアップは左から155px の位置に開き、入力欄の左端(54px)ではなく打っている文字の隣に現れました。候補は12個から2個へ絞られ、aria-activedescendant は下矢印1回で ac-opt0 から ac-opt1 へ移り、Enter を押すと打ちかけの断片が document.querySelectorAll へまるごと置き換わります。

08エラーの波線と吹き出し

括弧が閉じていない・色コードの桁が違う位置に赤い波線が引かれ、カーソルを重ねると理由が出る、貼り付けたコードをその場で点検する欄です。

エラーの波線と吹き出し
新しいタブで開く

08 · エラーの波線と吹き出し — 計測 累積面積 32.71% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

text-decoration-styletext-decoration-skip-inkaria-describedby

規則2つの簡易リント

function lint(text) {
  var errs = [], m;
  var hexRe = /#[0-9a-zA-Z]+/g;
  while ((m = hexRe.exec(text)) !== null) {
    var body = m[0].slice(1);
    var ok = /^[0-9a-fA-F]+$/.test(body) && HEX_DIGITS.indexOf(body.length) >= 0;
    if (!ok) errs.push({ start: m.index, end: m.index + m[0].length, msg: MSG_HEX });
  }
  var stack = [];
  for (var i = 0; i < text.length; i++) {
    var ch = text.charAt(i);
    if (ch === '{' || ch === '(' || ch === '[') stack.push(i);
    else if (ch === '}' || ch === ')' || ch === ']') {
      if (stack.length) stack.pop();
      else errs.push({ start: i, end: i + 1, msg: MSG_BRACKET });
    }
  }
  return errs;
}

最初の画面では5桁の色コード #ff412 と閉じていない { の2件に波線が付き、正常なコードでは0件、#ff412 だけを残せば1件になります。下線は text-decoration: underline wavy の2語で、件数は下のバーに「エラー 2件」のような文字でも書かれ aria-describedby で入力欄に繋がります。

09エディタ テーマ切り替え

ライト・ダーク・ハイコントラストの3つを CSS 変数ひと束で差し替える、夜間作業の多い管理画面の部品です。

エディタ テーマ切り替え
新しいタブで開く

09 · エディタ テーマ切り替え — 計測 累積面積 61.065% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

data-theme--tok-keycolor-scheme

変数を登録しないと切り替えがにじむ

// 色変数を登録する — 登録された <color> 変数だけが keyframes の中で steps(1, end) により値をきっぱり切り替わる。
// 初期値はライト版(静的な基本状態)と同じ値
@property --ed-bg  { syntax: '<color>'; inherits: true; initial-value: #{$stage-paper}; }
@property --ed-fg  { syntax: '<color>'; inherits: true; initial-value: #{$subject-ink}; }
@property --tok-key { syntax: '<color>'; inherits: true; initial-value: #{$subject-blue-deep}; }
@property --tok-str { syntax: '<color>'; inherits: true; initial-value: #7440d4; }
@property --tok-num { syntax: '<color>'; inherits: true; initial-value: #{$error}; }
@property --tok-com { syntax: '<color>'; inherits: true; initial-value: #6b6577; }

JS がするのは data-theme の1文字を書き換えることだけで、色6つと color-scheme はセレクタが入れ替えます。ハイコントラスト版は文字ごとにコントラスト比を測って本文 21.0:1、キーワード 14.54:1、文字列 11.88:1、数値 11.37:1、コメント 12.92:1 と揃えました。

html css js エディタ はライブラリなしでどこまで作れるか?

ここまでがライブラリなしで済む範囲です。複数ファイル、やり直しの履歴管理、言語サーバーとの連動のように文書モデルが要る仕事になると、専用エディタのほうが向いています。逆に入力欄ひとつに色・番号・括弧・検索・補完だけ載せるなら、テキストエディタ 作り方 の材料は <textarea> と <pre> の2要素で足ります。やり直しも貼り付けもフォーカスも textarea が受け持ったままなので、自分で書き足すのは色を塗り直す部分だけです。折り返しの切り替えやマルチカーソルは文書モデル側の仕事なので、この9つには含めていません。受け取ったファイルをそのまま開けばブラウザで動きます。

続きは コードブロック UI 9選 が読み取り専用のコード表示を、ターミナル・CLI UI 9選 がコマンドライン画面を扱います。仕様は MDN の textarea の解説 と setSelectionRange の解説 を参照してください。

どこで壊れるか — 落とし穴

グリッドで見える動きは CSS キーフレームで、人が打つ瞬間は JS の経路です。両者は別のコードなので、片方だけ生きていても測定は通ります。3つとも画面を動かして初めて見つかった壊れ方で、コードを読むだけでは分かりませんでした。

動いていない同期コードが黙って通る

関数そのものは正しく書けていても、動かす相手を間違えれば画面は何も変わりません。02 は本文がスクロールしたらガターを同じ値で押す関数を持っていたのに、textarea を内容の高さまで伸ばしてしまったせいで、スクロールしていたのは textarea ではなく外側の箱でした。textarea の高さをグリッドのマスに縛って初めて、本文40px に対してガターが -40px 追いかけました。05 のミニマップのように座標を自分で計算して描く画面は、css グラフ をSVGで描く9種と同じ罠を共有します — 描く側は正しいのに、測る相手を間違えている場合です。圧縮ファイル9つのパスワードは 8j2t7h4k で、同じ理由から9つすべて .is-demo を外した静的経路と実際のキー入力を別々に測っています。

steps のコマ送りは区間の終わりでだけ値を変える

01 の初版は 0% { opacity: 0 } 100% { opacity: 1 } に steps(1, end) を掛けていました。1区間のコマ送りは区間の終わりでしか値を変えないので2秒間ずっと透明で、代表画像が空の画面になり、動いていたフレームは 4/23、面積の合計は 0.26% でした。3% の位置に切り替え地点を置くと面積は 25.07%、動くフレームは 15/23 へ上がりました。06 も同じ落とし穴なので、一致の帯は最初から点けておき、動きは現在位置の移動で作りました。

状態の文字を JS だけが書くと止まる

04・05・09 の状態の文は JS が textContent で書くので、プレビューのループではその部分が動かず最初の文のまま止まりました。代表画像は「1行目にカーソル」と出しているのに、画面には3〜5行目の選択帯が敷かれた場面になりました。文を2枚重ねて CSS で切り替え、05 のように移動が連続するものはプレビューの文から数字を外しました。

3つのバリエーションとアクセシビリティ

変数3つを替えるだけで、9つの性格が一緒に変わります。

名前 書き換える値 変わる感じ
duration $dur-loop 2s → 1s タイピングとポップアップが2倍速くなる
easing $ease-spring → $ease-pop ポップアップとバーがもう1回跳ねる
color $subject-blue → $stage-yellow トークンの強調色が寒色から暖色へ

入力はすべて本当の <textarea> なので、スクリーンリーダー・IME・やり直しの経路がそのまま生きています。07 のポップアップは role="listbox" と aria-activedescendant で知らせつつフォーカスは入力欄に残して打ち続けられ、08 はエラー件数を文字で書いて aria-describedby で繋ぎます。prefers-reduced-motion: reduce では9つすべてのプレビューのループとカーソルの点滅が止まり、それぞれの部品がいちばん読める静止画が1枚残ります。9つの画面の文字60個分のコントラストを描画後の色で測って AA 未達は0個、最小は 4.9:1 でした。

FAQ

Monaco や CodeMirror を使う方が良いのでは

複数ファイルと言語サーバーが要るならその方が合っています。入力欄ひとつに色と番号だけ欲しいのに数百 KB のライブラリを読み込むのは重すぎます。9つから必要なものだけ選んで貼れば、依存は増えません。

html css テキスト エディタ で日本語を打つと変換中の文字は壊れませんか

本当の入力が <textarea> のままなので、変換はブラウザが処理します。contenteditable に替えると変換中の DOM に触れて文字が切れることがありますが、この作りでは下の層を塗り直すだけなので入力の経路には触れません。編集せず読むだけのコード窓で足りるなら、コードブロック UI 9選のほうが軽い答えになります。

波線の下線は古いブラウザでも見えますか

text-decoration-style は caniuse の実測で世界全体の 96.65%(完全 0.03% + 部分 96.62%)が使え、Chrome 57+、Edge 79+、Safari 7.1+、Firefox 6+ で動きます(2026-09-13 確認)。Internet Explorer と Opera Mini では使えないので、エラー件数を文字でも書いておく方が安全です。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。