-
Notifications
You must be signed in to change notification settings - Fork 756
Description
Background
Some properties support sub-pixels. WebKit.org has a breakdown in the LayoutUnit - Use cases section of their site.
Other properties, like border-width, don't support sub-pixels. John Resig - Sub-Pixel Problems in CSS is a short read that summarizes the issue well.
In Distance Units: the <length> type, the specification describes the method of rounding computed values such as border-width that don't use sub-pixels:
To snap a length as a border width given a
<length>len:
- Assert:
lenis non-negative.- If
lenis an integer number of device pixels, do nothing.- If
lenis greater than zero, but less than 1 device pixel, roundlenup to 1 device pixel.- If
lenis greater than 1 device pixel, round it down to the nearest integer number of device pixels.
This is the intended and currently-implemented behavior, and I'm not proposing that it changes for elements rendered on-screen.
Proposal
Implement an optional, opt-in feature to programmatically retrieve sub-pixel values when using functions such as getComputedStyle.
// The style is calculated using sub-pixels for all properties. Does not change the rendering behavior.
const computedStyle = getComputedStyle(element, options: { useSubPixels: true })This would not change the behavior of how elements are rendered. Properties like border-width that don't render with sub-pixels would still not render with sub-pixels.
This would simply allow developers to retrieve the sub-pixel values of properties such as border-width before they are rounded. The browsers have access to this information, and it would be helpful if developers were able to programmatically access it too.
useSubPixels could apply to all properties, or there could be more granularity. I'm most interested in it applying to border-width.
Reproducible Example
The contents of these two files are included below:
puppeteer-script.jspackage.json
The script uses Puppeteer, a library created and maintained by the Chrome DevTools team, to demonstrate how border-width values change due to sub-pixel rounding.
Puppeteer needs to run in a server environment, so the script can't be run in the browser. I've included detailed instructions on how to set up and run this script, in case anyone is reading this but isn't familiar with all the tools being used. If you're familiar with everything, you could skip reading these steps and go straight to running the script. The instructions are for Windows, but the process should be similar on other operators systems.
Steps to Run the Script
-
Create a folder anywhere on your computer, and save the attached
puppeteer-script.jsin that folder. -
Install the latest stable version of Node.js by using the prebuilt installer found here. No changes beyond the default installation are necessary.
-
Open the command line interface by typing
cmdorCommand Promptin your Windows Start menu/search field. -
In the command line interface, verify that Node has been globally installed by typing
node -vand pressing enter. Then, type and enternpm -v(npm is the Node Package Manager). Both commands should display the version number of the installed programs. -
Once installation is confirmed, in the command line interface, change directories so you're in the folder created in step 1. For example, if you created the folder on your Desktop and named it Test, you could type
cd Desktop/Testto navigate there. -
Once you've navigated to the folder where
puppeteer-script.jsis saved, type and enter the commandnpm install puppeteerto install the latest version of Puppeteer. Once the installation is complete, the folder should have a few new items: anode_modulesfolder, apackage.jsonfile, and apackage-lock.jsonfile. -
Update your
package.jsonfile to look like the version I've included here by adding"type": "module",to the JSON object. This will make it so you can callimport puppeteer from 'puppeteer'in the script. -
In the command line interface (still within the folder you created in step 1), type
node puppeteer-script.jsto run the script.
The command line interface should display the following:
border-left-width would have been 1800px * 0.0025 = 4.50px if sub-pixels were enabled
border-left-width is actually 4px since it has been rounded down
The computed width is 400.5px and the rect width is 400.5px
Explanation of the Script
The script created a page with a width of 1800px, and an element with border-left-width: 0.25vw, which means 0.25% of the screen's width. The sub-pixel value of border-left-width is 1800px * 0.0025 = 4.50px. However, per the specification, this value was rounded down to 4px, as expected.
For a bit of added information, the script also shows that the element's width based on the results of getComputedStyle and getBoundingClientRect uses sub-pixels. This was added to show that it would be consistent with the behavior of other properties if some properties, like border-width, could also return sub-pixels on the developer side. border-width contributes to the width of the element, and while its possible to use calculate the sub-pixel value based on information like this, it would be much more straight-forward and consistent to be able to retrieve it directly.
Script Code
puppeteer-script.js:
'use strict'
import puppeteer from "puppeteer"
(async function main()
{
try
{
const html = `<div id = "something"></div>`
const css = `#something
{
box-sizing: border-box;
width: 22.25vw;
border-top: 96px solid blue;
border-left: 0.25vw solid red;
position: absolute;
}`
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
await page.setViewport({ width: 1800, height: 2400 })
await page.setContent(html)
await page.addStyleTag({ content: css })
const results = await page.evaluate(() =>
{
const element = document.getElementById('something')
const computedStyle = getComputedStyle(element)
const borderLeftWidth = computedStyle.getPropertyValue('border-left-width')
const computedWidth = computedStyle.getPropertyValue('width')
const rectWidth = element.getBoundingClientRect().width
return {
borderLeftWidth: borderLeftWidth,
computedWidth: computedWidth,
rectWidth: rectWidth
}
})
const { borderLeftWidth, computedWidth, rectWidth } = results
console.log(`\nborder-left-width would have been 1800px * 0.0025 = 4.50px if sub-pixels were enabled`)
console.log(`\nborder-left-width is actually ${borderLeftWidth} since it has been rounded down`)
console.log(`\nThe computed width is ${computedWidth} and the rect width is ${rectWidth}px`)
await page.close()
await browser.close()
}
catch (error)
{
console.error(error)
}
})()package.json:
{
"type": "module",
"dependencies":
{
"puppeteer": "^22.15.0"
}
}Example of Proposed Feature
If this feature were added, the element created in the script above would still render with a border-left-width of 4px, but I would be able to call the following:
// The style is calculated using sub-pixels for all properties. Does not change the rendering behavior.
const computedStyle = getComputedStyle(element, options: { useSubPixels: true })
const borderLeftWidth = computedStyle.getPropertyValue('border-left-width')And receive a value of 4.50px for border-left-width - the sub-pixel value before rounding. The rendered border-left-width is still 4px, but the useSubPixels parameter allows me to compute the value as if it hadn't been rounded. Since the browser already has this information, it will hopefully not be difficult to implement.