Middle truncation with Pretext
Pixel-perfect middle truncation using the Pretext library for accurate text measurement, no DOM reflow required.
const STRINGS = [
{
label: "File path",
text: "/Users/chase/Repositories/my-project/src/components/navigation/TopBar.tsx",
},
{
label: "URL",
text: "https://github.com/chenglou/pretext/blob/main/src/layout.ts?tab=readme-ov-file#middle-truncation",
},
{
label: "Git commit",
text: "feat(auth): implement OAuth2 PKCE flow with refresh token rotation and silent renewal",
},
{
label: "Email",
text: "very.long.email.address.with.many.parts@some-extremely-long-domain-name.organization.com",
},
]
const FONT = "13px 'SF Mono', 'Fira Code', 'Cascadia Code', monospace"
const LINE_HEIGHT = 20
const CDN_URL =
"https://cdn.jsdelivr.net/npm/@chenglou/[email protected]/dist/layout.js"
function middleTruncate(text, maxWidth, { prepare, layout }) {
function fits(str) {
return layout(prepare(str, FONT), maxWidth, LINE_HEIGHT).lineCount === 1
}
if (fits(text)) return { prefix: text, suffix: null }
const ellipsis = "…"
if (!fits(ellipsis)) return { prefix: "", suffix: null }
let lo = 0,
hi = text.length
while (lo < hi) {
const mid = Math.floor((lo + hi + 1) / 2)
const half = Math.floor(mid / 2)
const candidate =
text.slice(0, half) + ellipsis + text.slice(text.length - (mid - half))
if (fits(candidate)) lo = mid
else hi = mid - 1
}
const half = Math.floor(lo / 2)
return {
prefix: text.slice(0, half).trimEnd(),
suffix:
lo - half > 0 ? text.slice(text.length - (lo - half)).trimStart() : "",
}
}
const styles = (
<style>{`
* { box-sizing: border-box; }
.body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.card {
overflow: hidden;
width: 100%;
background: white;
}
.demo-area {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.slider-row {
width: 100%;
height: 10px;
display: flex;
align-items: center;
margin-bottom: 0.25rem;
}
.track {
width: 100%;
height: 2px;
background: #e7e5e4;
border-radius: 1px;
position: relative;
}
.fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: #a8a29e;
border-radius: 1px;
pointer-events: none;
}
.thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 14px;
height: 14px;
border-radius: 50%;
background: white;
border: 2px solid #78716c;
cursor: ew-resize;
transition: border-color 0.1s, box-shadow 0.1s;
}
.thumb:hover, .thumb.dragging {
border-color: #44403c;
box-shadow: 0 0 0 3px rgba(68,64,60,0.12);
}
.item {
padding: 0.5rem 0;
border-bottom: 1px solid #f5f5f4;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.item:last-child { border-bottom: none; padding-bottom: 0; }
.item:first-child { padding-top: 0; }
.item-label {
font-size: 10px;
font-weight: 500;
color: #d6d3d1;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.item-text {
font-size: 13px;
color: #44403c;
white-space: nowrap;
overflow: hidden;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
line-height: 1.5;
}
`}</style>
)
function MiddleTruncatedText({ text, maxWidth }) {
const [pretext, setPretext] = React.useState(null)
React.useEffect(() => {
import(/* @vite-ignore */ CDN_URL).then(setPretext)
}, [])
const result =
pretext && maxWidth > 0 ? middleTruncate(text, maxWidth, pretext) : null
if (!result || result.suffix === null) return result ? result.prefix : text
return (
<>
{result.prefix}…{result.suffix}
</>
)
}
export default function Demo() {
const trackRef = React.useRef(null)
const [ratio, setRatio] = React.useState(0.65)
const [maxWidth, setMaxWidth] = React.useState(0)
const [dragging, setDragging] = React.useState(false)
React.useEffect(() => {
function updateMaxWidth() {
if (trackRef.current) {
setMaxWidth(trackRef.current.getBoundingClientRect().width * ratio)
}
}
updateMaxWidth()
window.addEventListener("resize", updateMaxWidth)
return () => window.removeEventListener("resize", updateMaxWidth)
}, [ratio])
React.useEffect(() => {
if (!dragging) return
function onMove(e) {
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const rect = trackRef.current.getBoundingClientRect()
setRatio(Math.min(1, Math.max(0.1, (clientX - rect.left) / rect.width)))
}
function onUp() {
setDragging(false)
}
window.addEventListener("mousemove", onMove)
window.addEventListener("mouseup", onUp)
window.addEventListener("touchmove", onMove)
window.addEventListener("touchend", onUp)
return () => {
window.removeEventListener("mousemove", onMove)
window.removeEventListener("mouseup", onUp)
window.removeEventListener("touchmove", onMove)
window.removeEventListener("touchend", onUp)
}
}, [dragging])
return (
<div>
{styles}
<div className="body">
<div className="card">
<div className="demo-area">
<div className="slider-row">
<div className="track" ref={trackRef}>
<div className="fill" style={{ width: `${ratio * 100}%` }} />
<div
className={`thumb${dragging ? " dragging" : ""}`}
style={{ left: `${ratio * 100}%` }}
onMouseDown={(e) => {
setDragging(true)
e.preventDefault()
}}
onTouchStart={(e) => {
setDragging(true)
e.preventDefault()
}}
/>
</div>
</div>
<div style={{ overflow: "hidden", maxWidth }}>
{STRINGS.map(({ label, text }) => (
<div className="item" key={label}>
<div className="item-label">{label}</div>
<div className="item-text">
<MiddleTruncatedText text={text} maxWidth={maxWidth} />
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
export const meta = {
title: "Middle truncation with Pretext",
description:
"Pixel-perfect middle truncation using the [Pretext](https://github.com/chenglou/pretext) library for accurate text measurement, no DOM reflow required.",
tags: ["ui"],
}