Skip to content

Instantly share code, notes, and snippets.

@twofingerrightclick
Created December 16, 2025 06:35
Show Gist options
  • Select an option

  • Save twofingerrightclick/fdaa7844cb52e12d2e9ed2dbbb928f9c to your computer and use it in GitHub Desktop.

Select an option

Save twofingerrightclick/fdaa7844cb52e12d2e9ed2dbbb928f9c to your computer and use it in GitHub Desktop.
Middle Ellipsis Truncation Web Component
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Middle Ellipsis Web Component</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 2rem;
max-width: 800px;
margin: 0 auto;
line-height: 1.5;
}
.container {
border: 1px solid #ccc;
padding: 1rem;
margin-bottom: 2rem;
resize: horizontal;
overflow: auto;
width: 300px; /* Initial width */
background: #f9f9f9;
}
h2 {
margin-top: 0;
}
/* Demo specific styles */
.ellipsis-box {
margin-bottom: 0.5rem;
background: white;
border: 1px solid #eee;
}
</style>
</head>
<body>
<h1>Middle Truncation Web Component</h1>
<p>Resize the containers below to see the truncation in action.</p>
<div class="container">
<h2>Script Files (Monospace)</h2>
<middle-ellipsis class="ellipsis-box">
<code style="font-family: monospace"
>The_Princess_Bride_Script_Final_Draft_v4_Revised_By_Goldman.pdf</code
>
</middle-ellipsis>
<middle-ellipsis class="ellipsis-box">
<code style="font-family: monospace"
>Battle_of_Wits_Scene_Vizzini_vs_Westley_☠️_Poison.docx</code
>
</middle-ellipsis>
<middle-ellipsis class="ellipsis-box">
<code style="font-family: monospace"
>Rodents_Of_Unusual_Size_Location_Scouting_Photos_Fire_Swamp.jpg</code
>
</middle-ellipsis>
</div>
<div
class="container"
style="
width: 400px;
font-family: 'American Typewriter', 'Baskerville', Georgia, serif;
"
>
<h2>Dialogue & Assets (Variable Fonts)</h2>
<middle-ellipsis class="ellipsis-box">
<span style="font-weight: bold"
>Inigo_Montoya_Revenge_Speech_Draft_1_Hello_My_Name_Is.txt</span
>
</middle-ellipsis>
<div
style="
font-family: 'Chalkboard SE', 'Comic Sans MS', sans-serif;
margin-top: 10px;
"
>
<middle-ellipsis class="ellipsis-box">
<span>Mawage_Is_Wot_Bwings_Us_Togedder_Today_Speech_📜.rtf</span>
</middle-ellipsis>
</div>
<div
style="
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-weight: bold;
margin-top: 10px;
"
>
<middle-ellipsis class="ellipsis-box">
<span
>Miracle_Max_Resurrection_Pill_Ingredients_List_🍫_Chocolate_Coating.xlsx</span
>
</middle-ellipsis>
</div>
</div>
<script>
/**
* Singleton for measuring text width using an off-screen canvas.
* Fast compared to DOM measurement.
*/
const TextMeasurer = (() => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
return {
width(text, font) {
if (ctx.font !== font) {
ctx.font = font;
}
return ctx.measureText(text).width;
},
};
})();
/**
* Singleton for splitting text into graphemes (handling emojis correctly).
*/
const GraphemeSplitter = (() => {
let segmenter;
try {
segmenter = new Intl.Segmenter({ granularity: "grapheme" });
} catch (e) {}
return {
split(text) {
if (segmenter) {
return Array.from(segmenter.segment(text), (s) => s.segment);
}
return Array.from(text);
},
};
})();
class MiddleEllipsis extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
overflow: hidden;
white-space: nowrap;
width: 100%;
}
::slotted(*) {
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
}
</style>
<slot></slot>
`;
}
connectedCallback() {
// Observe resize of the component itself
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.render(entry.contentRect.width);
}
});
this.resizeObserver.observe(this);
// Listen for slot changes (new content)
this.shadowRoot
.querySelector("slot")
.addEventListener("slotchange", () => this.render());
}
disconnectedCallback() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
/**
* Main render logic.
* @param {number} [width] - The available width.
*/
render(width) {
const slot = this.shadowRoot.querySelector("slot");
const elements = slot.assignedElements();
if (elements.length === 0) return;
// Operate on the first element assigned to the slot
const target = elements[0];
// Store original text on the element itself to avoid data loss
if (!target._originalText) {
target._originalText = target.textContent.trim();
}
const text = target._originalText;
const containerWidth =
width !== undefined ? width : this.getBoundingClientRect().width;
if (containerWidth === 0) return;
// Measure using the TARGET element's styles
const style = window.getComputedStyle(target);
const font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
// Optimization: If the full text fits, just show it
const fullWidth = TextMeasurer.width(text, font);
if (fullWidth <= containerWidth) {
target.textContent = text;
target.title = ""; // Clear tooltip
return;
}
// Otherwise, calculate the truncated string
target.textContent = this.middleTruncate(text, containerWidth, font);
target.title = text; // Show full text on hover
}
/**
* 50/50 split middle truncation based on rendered width.
*/
middleTruncate(text, containerWidth, font) {
const ellipsis = "…";
const ellipsisWidth = TextMeasurer.width(ellipsis, font);
// Available space for text (total - ellipsis)
const availableWidth = containerWidth - ellipsisWidth;
// Target width for each side (50% of available space)
const halfWidth = Math.max(0, availableWidth / 2);
// Split into graphemes to handle emojis correctly
const graphemes = GraphemeSplitter.split(text);
// Binary search for the left part
let lo = 0,
hi = graphemes.length;
let leftCount = 0;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const substr = graphemes.slice(0, mid).join("");
if (TextMeasurer.width(substr, font) <= halfWidth) {
leftCount = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
const left = graphemes.slice(0, leftCount).join("");
// Binary search for the right part
lo = 0;
hi = graphemes.length;
let rightCount = 0;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const substr = graphemes.slice(graphemes.length - mid).join("");
if (TextMeasurer.width(substr, font) <= halfWidth) {
rightCount = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
const right = graphemes.slice(graphemes.length - rightCount).join("");
return `${left}${ellipsis}${right}`;
}
}
customElements.define("middle-ellipsis", MiddleEllipsis);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment