Skip to content

Instantly share code, notes, and snippets.

@softmarshmallow
Created March 8, 2026 14:02
Show Gist options
  • Select an option

  • Save softmarshmallow/30c237d49acaf4534d8687944270d94f to your computer and use it in GitHub Desktop.

Select an option

Save softmarshmallow/30c237d49acaf4534d8687944270d94f to your computer and use it in GitHub Desktop.
how ellipse behaves with content editable.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>contenteditable clamp / ellipsis demo</title>
<style>
:root {
--w: 420px;
--lh: 1.4;
--lines: 3;
--border: #cfcfcf;
--focus: #3b82f6;
--bg: #ffffff;
--muted: #666;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 24px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fafafa;
color: #111;
}
.app {
max-width: 980px;
margin: 0 auto;
}
.controls {
display: grid;
grid-template-columns: repeat(6, max-content);
gap: 12px 16px;
align-items: end;
margin-bottom: 20px;
padding: 16px;
border: 1px solid var(--border);
background: var(--bg);
border-radius: 12px;
}
.field {
display: grid;
gap: 6px;
}
.field label {
font-size: 12px;
color: var(--muted);
}
select,
input,
button,
textarea {
font: inherit;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: white;
}
button {
cursor: pointer;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.panel {
border: 1px solid var(--border);
background: var(--bg);
border-radius: 12px;
padding: 16px;
}
.panel h2 {
margin: 0 0 8px;
font-size: 16px;
}
.hint {
margin: 0 0 14px;
font-size: 13px;
color: var(--muted);
}
.demo-wrap {
width: var(--w);
max-width: 100%;
}
.demo {
width: 100%;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
outline: none;
background: white;
line-height: var(--lh);
min-height: calc(1em * var(--lh) * var(--lines) + 24px);
text-align: left;
white-space: normal;
word-break: break-word;
}
.demo:focus {
border-color: var(--focus);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
}
/* not focused: clamp mode */
.mode-hide:not(.focus-remove) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines);
overflow: hidden;
}
.mode-hide.focus-remove:not(:focus) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--lines);
overflow: hidden;
}
/* focused: clamp removed */
.mode-hide.focus-remove:focus {
display: block;
-webkit-line-clamp: unset;
overflow: auto;
max-height: none;
}
/* scroll mode */
.mode-scroll {
overflow-y: auto;
max-height: calc(1em * var(--lh) * var(--lines) + 24px);
}
.stats {
margin-top: 10px;
font-size: 12px;
color: var(--muted);
}
.state {
margin-top: 10px;
font-size: 12px;
color: #333;
}
textarea {
width: 100%;
min-height: 220px;
resize: vertical;
line-height: 1.4;
}
@media (max-width: 940px) {
.controls {
grid-template-columns: 1fr 1fr;
}
.row {
grid-template-columns: 1fr;
}
.demo-wrap {
width: 100%;
}
}
</style>
</head>
<body>
<div class="app">
<div class="controls">
<div class="field">
<label for="align">Align</label>
<select id="align">
<option value="left">left</option>
<option value="center" selected>center</option>
<option value="right">right</option>
</select>
</div>
<div class="field">
<label for="lines">Max lines</label>
<input id="lines" type="number" min="1" max="12" value="3" />
</div>
<div class="field">
<label for="overflowMode">Overflow mode</label>
<select id="overflowMode">
<option value="hide" selected>hide / clamp</option>
<option value="scroll">scroll</option>
</select>
</div>
<div class="field">
<label for="focusBehavior">On focus</label>
<select id="focusBehavior">
<option value="keep">keep overflow behavior</option>
<option value="remove" selected>remove clamp / ellipsis</option>
</select>
</div>
<div class="field">
<label for="dummy">Dummy text</label>
<select id="dummy">
<option value="short">short</option>
<option value="medium" selected>medium</option>
<option value="long">long</option>
<option value="cjk">cjk</option>
<option value="mixed">mixed</option>
</select>
</div>
<button id="loadBtn" type="button">Load dummy text</button>
</div>
<div class="row">
<section class="panel">
<h2>Interactive demo</h2>
<p class="hint">
Click into the box. In clamp mode with “remove clamp / ellipsis”, the text is clamped when blurred, then expands when focused.
</p>
<div class="demo-wrap">
<div
id="demo"
class="demo mode-hide focus-remove"
contenteditable="true"
spellcheck="false"
></div>
</div>
<div id="state" class="state"></div>
<div id="stats" class="stats"></div>
</section>
<section class="panel">
<h2>Source text mirror</h2>
<p class="hint">
Edit here or in the demo box.
</p>
<textarea id="source"></textarea>
</section>
</div>
</div>
<script>
const demo = document.getElementById("demo");
const source = document.getElementById("source");
const align = document.getElementById("align");
const lines = document.getElementById("lines");
const overflowMode = document.getElementById("overflowMode");
const focusBehavior = document.getElementById("focusBehavior");
const dummy = document.getElementById("dummy");
const loadBtn = document.getElementById("loadBtn");
const stats = document.getElementById("stats");
const state = document.getElementById("state");
const samples = {
short:
"Short centered editable text.",
medium:
"This is a center-aligned editable paragraph used to demonstrate multiline truncation with contenteditable. Click inside, edit it, and switch between clamp mode and scroll mode.",
long:
"This is a longer editable paragraph intended to exceed multiple lines inside the demo box. The purpose is to observe how contenteditable interacts with line clamping, center alignment, and overflow behavior. In clamp mode, the box visually truncates at the chosen maximum line count. If focus behavior is set to remove clamp, the full text becomes visible once the editable region is focused.",
cjk:
"이 예시는 여러 줄 말줄임과 contenteditable, 그리고 가운데 정렬이 함께 있을 때 어떤 식으로 보이는지 확인하기 위한 데모입니다. 텍스트를 직접 수정해 보고, 최대 줄 수를 바꾸고, 포커스 시 말줄임이 사라지도록도 설정해 보세요.",
mixed:
"Hello 안녕하세요 こんにちは 你好 — this mixed paragraph includes Latin, CJK, punctuation, and longer phrasing so you can inspect wrapping, alignment, editing behavior, and focus-time unclamping in a constrained multiline editable region."
};
function setLines(n) {
const safe = Math.max(1, Math.min(12, Number(n) || 1));
document.documentElement.style.setProperty("--lines", String(safe));
lines.value = safe;
}
function setAlign(value) {
demo.style.textAlign = value;
}
function setMode(mode) {
demo.classList.remove("mode-hide", "mode-scroll");
demo.classList.add(mode === "scroll" ? "mode-scroll" : "mode-hide");
}
function setFocusBehavior(value) {
demo.classList.toggle("focus-remove", value === "remove");
}
function syncFromDemo() {
source.value = demo.innerText;
updateStats();
}
function syncFromSource() {
demo.innerText = source.value;
updateStats();
}
function updateState() {
const mode = overflowMode.value;
const fb = focusBehavior.value;
const focused = document.activeElement === demo;
let msg = `mode: ${mode}`;
if (mode === "hide") {
msg += ` · on focus: ${fb === "remove" ? "clamp removed" : "clamp kept"}`;
} else {
msg += ` · on focus: scroll behavior remains`;
}
msg += ` · focused: ${focused ? "yes" : "no"}`;
state.textContent = msg;
}
function updateStats() {
const text = demo.innerText || "";
const chars = text.length;
const lineHeight = parseFloat(getComputedStyle(demo).lineHeight);
const renderedLines = Math.round((demo.scrollHeight / lineHeight) * 10) / 10;
stats.textContent =
`chars: ${chars} · scrollHeight: ${demo.scrollHeight}px · approx rendered lines: ${renderedLines}`;
updateState();
}
function loadDummy() {
const text = samples[dummy.value] || samples.medium;
source.value = text;
demo.innerText = text;
updateStats();
}
align.addEventListener("change", () => {
setAlign(align.value);
updateStats();
});
lines.addEventListener("input", () => {
setLines(lines.value);
updateStats();
});
overflowMode.addEventListener("change", () => {
setMode(overflowMode.value);
updateStats();
});
focusBehavior.addEventListener("change", () => {
setFocusBehavior(focusBehavior.value);
updateStats();
});
loadBtn.addEventListener("click", loadDummy);
source.addEventListener("input", syncFromSource);
demo.addEventListener("input", syncFromDemo);
demo.addEventListener("focus", updateStats);
demo.addEventListener("blur", updateStats);
setAlign(align.value);
setLines(lines.value);
setMode(overflowMode.value);
setFocusBehavior(focusBehavior.value);
loadDummy();
window.addEventListener("resize", updateStats);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment