Fixing Subpixel Layout Rendering in Safari

Programming

When I was developing this site, I ran across a strange issue when I was coding the categories and tags that appear at the top of the site. It worked beautifully in Chrome and Firefox, but Safari didn’t seem to understand the idea that 100% width means the entire width of the page, not almost the entire width.

After doing some research I finally figured out what was going wrong: both Chrome and Firefox have subpixel layout support, but for some reason neither Safari 7 nor Safari on iOS 7 do. (This is baffling to me considering Safari is a WebKit browser just like Chrome.)

Essentially what’s going on here is Safari is dealing only in integer widths. So, for example, let’s assume we have a container with a width of 100%. Inside of this container are ten blocks floated to the left, each with a width of 10%. Now let’s assume the user’s browser is currently 993px wide. When Safari tries to calculate the width of each of these blocks, it calculates 993px/10 = 99.3px and then it rounds that value down to 99px. This means that the total width of all of the items will end up being 99px*10 = 990px, resulting in an extra 3px space on the right side of this container.

To avoid having the sum of the widths of the items be greater than the width of the viewport, Safari always rounds the items’ widths down. So the maximum error in pixels that Safari’s method will yield is the number of items minus one. For example, if the user’s browser width is now 999px, Safari calculates 999px/10 = 99.9px, which it then rounds down to 99px. So the total width of all the items is again 990px, but now we have an extra 9 pixels on the right.

The solution to this problem isn’t immediately clear, but fortunately there are a lot of really smart people out there who work on these things. The technical details behind subpixel layout rendering are complex (but fascinating). Subpixels not only allow us to create percentage-based layouts, but also allow us to have more fine-grained control over things like kerning. To learn more about about subpixel layouts, check out this article from WebKit.

A popular solution for browsers that don’t support subpixel layouts is simply filling the background of the container with the color of the background of the last item. This way, it will look like the last item is the same width as the other items plus the rounding error. Sometimes this looks reasonable, other times it doesn’t.

In the case of this website, I felt that that solution just wouldn’t cut it, so I created a new, JavaScript-based solution. Now, without further ado, I present my admittedly very hacky solution to make simple percentage-based layouts “work” in Safari:

function fixSubpixelLayout(container, items) {

	var width = container.width(),
		itemWidth = width / items.length,
		roundedItemWidth = Math.floor(itemWidth);

	// the fix is only necessary when the browser width isn't
	// evenly divisible by the number of items
	if(roundedItemWidth !== itemWidth) {

		var roundingError = width - (roundedItemWidth * items.length),
			itemCounter = 0,
			currItem;

		while(items.length > 0) {

			// alternate between the first item in the container...
			if(itemCounter++ % 2 === 0) {
				// get the first item in items
				currItem = items.get(0);
				// remove the first item from items
				items.splice(0,1);
			}

			// and the last item in the container
			else {
				// get the last item in items
				currItem = items.get(-1);
				// remove the last item from items
				items.splice(items.length-1, 1);
			}

			// adjust the width of the current item accordingly

			if(roundingError > 0) {
				$(currItem).width(roundedItemWidth + 1);
				roundingError--;
			}

			else {
				$(currItem).width(roundedItemWidth);
			}

		}

	}

	// when the fix isn't necessary, remove any current widths
	else {
		items.width('');
	}

}

To use this function, you’ll need to have jQuery included. This function accepts a container and the set of items in that container as arguments. After including the above function, to run it only requires a single line of code. For example, I use it to recalculate the widths of the category links at the top of this site:

fixSubpixelLayout($('#all-cats'), $('#all-cats .cat'));

This function begins by calculating the rounding error using Safari’s method for calculating item widths. It then iterates through all of the items in the container. But instead of simply iterating over the items starting with the first item until we reach the last item, it starts with the first item, then goes to the last item, then the second item, then the second-to-last item, and so on. I decided to add this extra complication because I wanted to try to avoid any visual indication that the items aren’t exactly the same width. Having the first half of the items potentially be wider than the second half could cause the rendering to look a little sloppy (especially in the case of this website). In each iteration, the width of the current item is increased by 1 pixel if we still have a positive rounding error (the rounding error is decremented each time an item’s width is incremented).

Finally, there are two more things that need to be considered. The first is that you need to be sure to also run this function whenever the browser is resized. This is easily handled with jQuery’s resize function. The second thing is that you should be careful to only run this function on browsers that don’t handle this issue natively: Safari and Safari on iOS. This can be handled a number of ways, but I prefer Conditionizr for its small size and elegant API.