Subpixel Rendering

TL;DR / When calculated CSS values fall between physical pixels, the browser must round, interpolate, or anti-alias — producing visual artifacts like 1px gaps, blurry edges, and misaligned borders.

How It Works

  Pixel grid vs. calculated position

  ┌──────────────┐                ┌────────────────┐
  │ 50% of 75px  │                │    Browser     │
  │   = 37.5px   │───────────────→│   rounds to    │
  └──────────────┘                │  37px or 38px  │
                                  └────────────────┘

  Result: 1px gaps, blurry edges


  ┌──────────────┐                ┌────────────────┐
  │  transform:  │                │  GPU renders   │
  │  translateX  │───────────────→│  at subpixel   │
  │   (37.5px)   │                │    position    │
  └──────────────┘                └────────────────┘

  Result: smooth but may look
  blurry on low-DPI

Edit diagram

Physical displays are made of discrete pixels. A CSS pixel maps to one or more physical pixels depending on the device pixel ratio (DPR), but CSS calculations can produce fractional values that do not align with the pixel grid. The browser must resolve this mismatch, and how it does so determines whether your layout looks crisp or subtly broken.

How Fractional Values Arise

Fractional pixel values are everywhere in CSS:

  • Percentages: width: 33.333% of a 100px container = 33.333px
  • Division: Three equal-width columns in a 100px container each get 33.333px
  • Calc: calc(100% / 7) for a 7-column grid
  • Viewport units: 50vw on an odd-width viewport
  • Transforms: translateX(50%) of an odd-width element
  • Flexbox: distributing remaining space among flex items
  • Line-height and font metrics: text rendering constantly produces fractional positions

How Browsers Handle Fractions

Browsers use different strategies depending on the property and rendering phase:

Layout rounding. For layout properties (width, height, top, left), the browser rounds calculated values to the nearest pixel (or sub-pixel unit, typically 1/60th or 1/64th of a CSS pixel). Different browsers use different rounding strategies — some round down, some round to nearest. This rounding means three columns of 33.333% might render as 33px + 33px + 34px, creating a visible 1px discrepancy.

Anti-aliasing. For text and borders that fall between pixels, the browser uses anti-aliasing — blending adjacent pixels to simulate the appearance of a sub-pixel position. This makes edges look smooth but can also make them look blurry, especially single-pixel lines.

GPU sub-pixel positioning. When transform is used, the GPU can position elements at true sub-pixel coordinates. Instead of rounding to the pixel grid, the GPU interpolates between pixels, producing smoother animation but potentially blurrier static results.

Common Visual Artifacts

1px gaps between elements. When a series of elements with percentage widths are laid out, rounding can cause their total to be less than the container width, producing a visible gap. This is especially common in grid layouts and navigation bars.

Border misalignment. Two adjacent elements with border: 1px solid may appear to have different border thicknesses if their calculated positions place the border at different sub-pixel offsets. On one element the border aligns with the pixel grid (crisp); on another it falls between pixels (blurry).

Text shimmer during animation. When an element is animated with transform: translate(), its text may appear to shimmer or vibrate as it passes through sub-pixel positions. Each frame, the GPU renders the text at a slightly different sub-pixel offset, changing the anti-aliasing pattern.

Blurry images. An image positioned at a fractional pixel offset is bilinearly interpolated by the GPU, softening its pixels. This is most noticeable with pixel art, icons, and small images where every pixel matters.

Strategies for Crisp Rendering

Use whole-pixel values. Where possible, set dimensions and positions to integer pixel values. Use Math.round() when calculating sizes in JavaScript.

Use transform: translate3d() with pixel-snapped values. For animations, round to whole pixels at the start and end positions. Sub-pixel movement during animation is usually acceptable; sub-pixel positioning at rest is where artifacts are visible.

Avoid percentage-based layouts for pixel-critical designs. Flexbox and grid with fr units distribute space in a way that the browser can resolve without rounding errors. Prefer 1fr 1fr 1fr over 33.333% 33.333% 33.333%.

Account for device pixel ratio. On 2x DPR screens, a CSS pixel is 2 physical pixels, so fractional CSS values resolve to whole physical pixels more often. On 1x screens, every 0.5px offset is a visible artifact. Design for the lowest DPR in your target audience.

Use image-rendering: pixelated for pixel art and icons that should not be anti-aliased when scaled.

High-DPI and the Disappearing Problem

On Retina/HiDPI displays (DPR 2 or 3), many sub-pixel artifacts become invisible because the physical pixel grid is fine enough that rounding errors are below the threshold of human perception. A 0.5px gap on a 2x display is a single physical pixel — often imperceptible. This is why sub-pixel issues are reported more frequently on 1080p monitors than on 4K displays.

Gotchas

  • border-width: 0.5px is supported in Safari and renders as a true half-pixel line on Retina displays. Other browsers round to 0px or 1px. Use with caution and a fallback.
  • transform creates a new stacking context and containing block. An element positioned with transform: translateX(0.5px) for alignment purposes gains additional side effects beyond the position shift.
  • Zoom levels create fractional pixels. At 90% or 110% browser zoom, even integer CSS pixel values resolve to fractional physical pixels. You cannot prevent this — design must tolerate rounding at non-standard zoom levels.
  • will-change: transform promotes an element to a GPU layer, which changes its text anti-aliasing from subpixel (crisp) to grayscale (slightly blurry). This can make text look different from surrounding non-promoted text.
  • Flexbox gap rounding can cause the last item in a flex row to wrap unexpectedly. If three items with flex-basis: 33.333% and gap: 1px exceed the container width after rounding, the third item drops to a new row. Use flex: 1 instead.