Skip to content

Instantly share code, notes, and snippets.

@Temzasse
Created March 31, 2025 10:41
Show Gist options
  • Save Temzasse/af3c007f80d3d4691ec8605586bfc74f to your computer and use it in GitHub Desktop.
Save Temzasse/af3c007f80d3d4691ec8605586bfc74f to your computer and use it in GitHub Desktop.
Get caret position in input / textarea
const properties = [
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'boxSizing',
'direction',
'fontFamily',
'fontSize',
'fontSizeAdjust',
'fontStretch',
'fontStyle',
'fontVariant',
'fontWeight',
'height',
'letterSpacing',
'lineHeight',
'overflowX',
'overflowY',
'paddingBottom',
'paddingLeft',
'paddingRight',
'paddingTop',
'tabSize',
'textAlign',
'textDecoration',
'textIndent',
'textTransform',
'width',
'wordSpacing',
] as const;
export function getCaretCoordinates(
element: HTMLTextAreaElement | HTMLInputElement
): { top: number; left: number; height: number } {
const position = element.selectionEnd;
if (position === null) {
return { top: 0, left: 0, height: 0 };
}
const div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);
const style = div.style;
const computed = window.getComputedStyle(element);
const isInput = element.nodeName === 'INPUT';
style.whiteSpace = 'pre-wrap';
if (!isInput) style.wordWrap = 'break-word';
style.position = 'absolute';
style.visibility = 'hidden';
properties.forEach(prop => {
if (isInput && prop === 'lineHeight') {
if (computed.boxSizing === 'border-box') {
const height = parseInt(computed.height);
const outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight);
style.lineHeight =
height > targetHeight
? `${height - outerHeight}px`
: computed.lineHeight;
} else {
style.lineHeight = computed.height;
}
} else {
style[prop] = computed[prop];
}
});
style.overflow = 'hidden';
div.textContent = element.value.substring(0, position);
if (isInput) {
// Replace single spaces with non-breaking spaces
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
}
const span = document.createElement('span');
span.textContent = element.value.substring(position) || '.';
div.appendChild(span);
const coordinates = {
top: span.offsetTop + parseInt(computed.borderTopWidth),
left: span.offsetLeft + parseInt(computed.borderLeftWidth),
height: parseInt(computed.lineHeight),
};
document.body.removeChild(div);
return coordinates;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment