Created
May 19, 2026 17:18
-
-
Save mrclay/5a8d434b41f8b2e80ec4129146d1a2ce to your computer and use it in GitHub Desktop.
React/web components to measure height of an element
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| /** | |
| * Measures the height of the previous element. | |
| * | |
| * This example will show the height of the P element, including any | |
| * padding/borders: | |
| * | |
| * <p>...</p> | |
| * <measure-height></measure-height> | |
| */ | |
| class MeasureHeightElement extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: "open" }); | |
| this._observer = null; | |
| this._onResize = () => this.update(); | |
| } | |
| connectedCallback() { | |
| this.update(); | |
| window.addEventListener("resize", this._onResize); | |
| const prevEl = this.previousElementSibling; | |
| if (!prevEl) { | |
| return; | |
| } | |
| this._observer = new ResizeObserver(() => this.update()); | |
| this._observer.observe(prevEl); | |
| } | |
| disconnectedCallback() { | |
| window.removeEventListener("resize", this._onResize); | |
| if (this._observer) { | |
| this._observer.disconnect(); | |
| this._observer = null; | |
| } | |
| } | |
| update() { | |
| const prevEl = this.previousElementSibling; | |
| if (!prevEl) { | |
| return; | |
| } | |
| const height = prevEl.getBoundingClientRect().height; | |
| const [whole, decimal] = String(Math.round(height * 1000) / 1000).split( | |
| ".", | |
| ); | |
| const rest = decimal ? `.${decimal}` : ""; | |
| const style = ` | |
| :host { | |
| background-color: #fff; | |
| background-image: linear-gradient(to left, #ccc, #ccc); | |
| background-size: 1px 100%; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| border-block: 1px solid #ccc; | |
| border-inline: 5px solid transparent; | |
| color: #000; | |
| display: block; | |
| font: 14px/17px system-ui, sans-serif; | |
| position: absolute; | |
| width: 20px; | |
| } | |
| span[a] { | |
| position: absolute; | |
| inset: calc(50% - 10px) -5rem calc(50% - 10px) -5rem; | |
| line-height: 20px; | |
| padding: 0 4px; | |
| text-align: center; | |
| transform: rotate(90deg); | |
| } | |
| span[b] { | |
| background: #fff; | |
| } | |
| span[c] { | |
| opacity: 0.5; | |
| } | |
| `; | |
| this.shadowRoot.innerHTML = ` | |
| <style>${style}</style> | |
| <span a><span b>${whole}<span c>${rest}</span></span></span> | |
| `; | |
| // Absolutely position the element to the top right of the previous sibling | |
| this.style.top = `${prevEl.offsetTop}px`; | |
| this.style.left = `${prevEl.offsetLeft + prevEl.offsetWidth}px`; | |
| this.style.height = `${prevEl.offsetHeight}px`; | |
| } | |
| } | |
| if (!customElements.get("measure-height")) { | |
| customElements.define("measure-height", MeasureHeightElement); | |
| } | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useEffect, useRef, useState } from "react"; | |
| /** | |
| * Measures the height of the preceding sibling element. | |
| * | |
| * This example will show the height of the P element, including any | |
| * padding/borders: | |
| * | |
| * <p>...</p> | |
| * <MeasureHeight /> | |
| */ | |
| function MeasureHeight() { | |
| const [height, setHeight] = useState<number>(0); | |
| const [position, setPosition] = useState({ top: 0, left: 0 }); | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| const el = containerRef.current; | |
| if (!el) { | |
| return; | |
| } | |
| const prevEl = el.previousElementSibling as HTMLElement; | |
| if (!prevEl) { | |
| return; | |
| } | |
| const update = () => { | |
| const rect = prevEl.getBoundingClientRect(); | |
| setHeight(rect.height); | |
| setPosition({ | |
| top: prevEl.offsetTop, | |
| left: prevEl.offsetLeft + prevEl.offsetWidth, | |
| }); | |
| }; | |
| // Initial update | |
| update(); | |
| const observer = new ResizeObserver(update); | |
| observer.observe(prevEl); | |
| window.addEventListener("resize", update); | |
| return () => { | |
| observer.disconnect(); | |
| window.removeEventListener("resize", update); | |
| }; | |
| }, []); | |
| const whole = Math.floor(height); | |
| // e.g., ".123" | |
| const rest = (height % 1).toFixed(3).substring(1); | |
| const styles: Record<string, React.CSSProperties> = { | |
| container: { | |
| position: "absolute", | |
| top: `${position.top}px`, | |
| left: `${position.left}px`, | |
| height: `${height}px`, | |
| width: "20px", | |
| backgroundColor: "#fff", | |
| backgroundImage: "linear-gradient(to left, #ccc, #ccc)", | |
| backgroundSize: "1px 100%", | |
| backgroundPosition: "center", | |
| backgroundRepeat: "no-repeat", | |
| borderBlock: "1px solid #ccc", | |
| borderInline: "5px solid transparent", | |
| color: "#000", | |
| font: "14px/17px system-ui, sans-serif", | |
| pointerEvents: "none", // Prevents interaction blocking | |
| }, | |
| spanA: { | |
| position: "absolute", | |
| inset: "calc(50% - 10px) -5rem calc(50% - 10px) -5rem", | |
| lineHeight: "20px", | |
| padding: "0 4px", | |
| textAlign: "center", | |
| transform: "rotate(90deg)", | |
| }, | |
| spanB: { | |
| background: "#fff", | |
| }, | |
| spanC: { | |
| opacity: 0.5, | |
| }, | |
| }; | |
| return ( | |
| <div ref={containerRef} style={styles.container}> | |
| <span style={styles.spanA}> | |
| <span style={styles.spanB}> | |
| {whole} | |
| <span style={styles.spanC}>{rest}</span> | |
| </span> | |
| </span> | |
| </div> | |
| ); | |
| } | |
| export default MeasureHeight; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment