Charts · 20

Floating point, at the pixel

Three columns at 33.333% leave a hairline of background showing. A card that animates in on a transform has a border that looks slightly out of focus. A basket adds three items at £0.10 and shows £0.30000000000000004. None of these is a bug in your code; they are what happens when numbers that cannot be represented meet a grid that can only be whole. Drag the widths and watch the browser measure itself.

1 · The seam

3 × 33.3333% of 1001px = 333.667px each. Measured: = 0.000px (-1001.000px short: a seam). Edges land at : on a 1× screen the browser can only paint at multiples of 1.00px, so each fractional edge is either snapped or blended.

2 · The fuzzy border

transform: none
translateX(0.50px)
margin-left: 0.50px

The same 1px border three times. A transform is applied at raster time and can sit between device pixels, so at 0.50px the edge is anti-aliased across two of them and reads as blurred. Layout positions (margin, left) are snapped to the pixel grid before painting, so the third box stays crisp. Zoom in with the OS magnifier to see it; on a 1× screen it is obvious at 0.5, on a 2× screen it is subtler because the grid is finer.

3 · The price

0.1 + 0.20.30000000000000004
0.1 + 0.70.7999999999999999
3 × 0.10.30000000000000004
(1.005).toFixed(2)1.00
19.99 × 359.97
0.1 × 3 in cents0.3
100 / 3 × 3100
2⁵³ + 19007199254740992

A double has 53 bits of mantissa: every integer up to 2⁵³ exactly, and almost no decimal fractions at all, because a tenth is a repeating fraction in binary the way a third is in decimal. 1.005 is stored as 1.00499999999999989…, so toFixed(2) honestly rounds it down. Money goes in integer minor units, or a decimal type; never in a float.

4 · The bits

sign0positive
exponent011111110112^-4
mantissa100110011001100110011001100110011001100110011001101052 bits, plus the implicit leading 1

Stored value, to 20 significant figures: 0.10000000000000000555. Not the number you typed: the nearest double to it.

Notes

Tile widths are measured with getBoundingClientRect, which returns the layout engine’s fractional values (CSS pixels are real numbers; layout in Chromium runs in 1/64 px units, in WebKit and Gecko in 1/60 px). Painting then happens on the device-pixel grid, so a fractional edge is either snapped (layout properties: widths, margins, positions) or rasterised with anti-aliasing (transforms, which are applied by the compositor after layout). That is why a translated element can look blurred and a positioned one cannot, and why will-change: transform on text sometimes makes it soft. Sums are IEEE 754 doubles as JavaScript evaluates them; the bit view decodes the actual stored word with a DataView, and the 20-significant-figure readout is what the double really holds. Sources: IEEE 754-2019; CSS Values and Units Level 4, section 6 (the px unit is a real number); Chromium LayoutUnit documentation; Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic (1991).