The Case of the Missing `rem`: A Sherlock Holmes Guide to CSS Units in JavaScript
Being the record of an investigation into CSS units, JavaScript geometry, and a very small missing value — as written by Dr. Watson. A Matter of No Consequence It began, as many of my friend's cases did, with
Being the record of an investigation into CSS units, JavaScript geometry, and a very small missing value — as written by Dr. Watson.
A Matter of No Consequence
It began, as many of my friend's cases did, with a problem I thought too small to matter.
"A dropdown," I said, handing Holmes the ticket. "It should sit 0.25rem below its button. An hour's work, no more."
Holmes did not look up.
"You find it trivial, Watson. Yet it has long been an axiom of mine that the little things are infinitely the most important."
He was right. He usually was — and he never let me forget it. That 0.25rem would lead us through the CSS value pipeline, the geometry of the browser, and to the border of JavaScript itself, where a unit had gone missing and nobody had reported it.
The Impossible Request
The dropdown already used Floating UI — a small library that calculates where a floating panel should sit relative to its button. The positioning worked. We only needed to nudge the panel a little below its button.
I wrote the natural thing:
offset('0.25rem')
The library refused it. Its offset() middleware expects a numeric distance:
offset(4)
"Ha!" I said. "The library is simply rude. JavaScript cannot understand relative units. Case closed, Holmes."
Holmes looked up at last.
"The documentation plainly says a number — that part you read correctly. But the design says 0.25rem. Knowing the API wants a number does not tell us which number. That question is the whole case."
He tapped the code with one finger.
"Look closer. The number it wants is bare — no unit attached. Its geometry runs in CSS pixels, the same unit getBoundingClientRect() reports. Every number it takes is understood to mean px. No "4px" string ever crosses the boundary."
"Then why does CSS accept the very same value?" I asked, and showed him:
.dropdown {
margin-top: 0.25rem;
}
Holmes smiled — the smile that meant the real work was starting.
"One door accepts the visitor. The other refuses him. The mystery is not at either door, Watson. It is at the border between them."
Clue the First: rem Is Not a Fixed Number
"The name tells us the first fact," said Holmes, writing on the hearth:
rem = root em
"One rem is based on the font size of the root element — usually <html>. If that size is 16px:"
1rem = 16px
0.25rem = 4px
"There," I said. "So it is 4px. Fixed. Done."
"You assume every visitor's browser agrees with your arithmetic, Watson. MDN describes rem as a root-relative length: the common default is 16px, but user preferences may change it. A reader with weak eyesight may set the root font size to 20px. A media query may change it. The application itself may change it."
He underlined his chalk:
0.25 × current root font size
"0.25rem is not a number, Watson. It is a recipe. And a recipe must be baked before it can be eaten."
Clue the Second: CSS Understands the Recipe
"Then the browser is the culprit," I said. "It cannot process relative units."
Holmes shook his head.
"Watch what the engine does with padding: 0.25rem. It knows four things:"
- The number is
0.25. - The unit is
rem. -
rempoints to the root font size. - So the final layout distance can be calculated.
"Relative units are not strangers in CSS — they are citizens. The browser resolves them during value processing: first into computed values, then into the final pixel geometry used for layout. MDN walks through this pipeline."
"So the browser is innocent," I said.
"Eliminated. And that is progress, Watson. When you have eliminated the impossible, whatever remains, however improbable, must be the truth. What remains is the strangest suspect of all: JavaScript itself."
Clue the Third: JavaScript Numbers Carry No Units
"In JavaScript," said Holmes, "this is a number:"
0.25
"It has no unit attached. And this is a string:"
'0.25rem'
"JavaScript can hold that string — but arithmetic does not know what it means:"
'0.25rem' * 2 // NaN
"There!" I said. "JavaScript cannot do the math. My own theory, Holmes — it cannot understand relative units."
Holmes looked at me with something between pity and patience.
"JavaScript is not bad at arithmetic, Watson. It multiplies perfectly — the moment it is given numbers. The problem is that an ordinary string carries no CSS meaning into a geometry calculation. The suspect was never arithmetic. The suspect is the border — the crossing between two realms: CSS, where values carry units, and JavaScript, where geometry speaks only in numbers."
He was quiet for a moment.
"JavaScript can ask the browser to resolve CSS values — through getComputedStyle(), or the more structured CSS Typed OM. The translation is possible. It is simply not automatic. And that, Watson, is where our criminal has been hiding all along: in the gap between what CSS says and what JavaScript can measure."
The Missing Translation
When Holmes finally wrote the fix, it was almost too small to believe:
const remToPx = (rem) =>
rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
Then:
offset(remToPx(0.25))
"If the root font size is 16px:"
0.25 × 16 = 4
"If it is 20px:"
0.25 × 20 = 5
"The result is a bare number — 4, never "4px". The unit lives in the meaning, not in the value."
I studied the little function. "So this fixes JavaScript's weakness with units."
"No, Watson. That theory died two clues ago. Nothing is wrong with JavaScript's arithmetic. This function translates: a CSS-relative length on one side, an absolute pixel number on the other. Every border needs a translator who speaks both languages."
"And this border has many crossings, Watson — canvas, scroll offsets, element measurements. All of them speak in bare numbers."
A Loose Thread
The case looked closed. Holmes, of course, did not think so.
"One thread remains, and it is the one a careless detective never pulls. Is this translation responsive? The function reads the root font size at the moment it runs:"
getComputedStyle(document.documentElement).fontSize
"Call it again after a font-size change, and it picks up the new value. But nothing calls it again on its own. If the result is cached — computed once at startup, or baked into a value created when the page loads — a later font-size change will be ignored until the code runs the conversion again."
"And our own dropdown, Holmes — which of its values are baked, and which stay fresh?"
"In the dropdown that began this case, the panel's maximum height is recalculated on every position update, so it always uses the current root font size. The offset and shift values are created once, when the positioning is set up, so they would only see a font-size change after a reload."
"Is there no way past this last hurdle, Holmes?"
"Floating UI's authors were not blind to the problem, Watson. The offset() middleware accepts not only a bare number, but a function that returns one:"
offset(() => remToPx(0.25))
"Pass the recipe instead of the baked dish, and Floating UI runs our translator on every position update. The gap now follows any font-size change while the page is open — no need to rebuild the middleware. The same trick works for shift()."
He wrote the rule beneath it:
CSS resolves rem dynamically.
JavaScript resolves it only when your code runs the conversion.
"And that, Watson, is why a matter you called trivial led through CSS value processing, browser geometry, and the internals of a positioning library."
"The rem was never missing at all, then," I said.
"It was never missing, Watson. It simply could not cross the border into JavaScript unescorted."
Case closed.
The Smallest Clues
I confess I had expected to waste the afternoon on a four-pixel gap.
Instead, I learned the lesson Holmes had been teaching me all afternoon: the tools you reach for tell you what you do not understand yet. I reached for a string because CSS had taught me that spacing carries units. The library refused it because geometry is spoken in numbers. Between those two truths stood a one-line translator — and behind it, a whole system I had never once thought to inspect.
The smallest clues sometimes open the largest cases.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.