Skip to content

[css-values-4][cssom] Proposal to allow retrieval of sub-pixel border values #10729

@ghost

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:

  1. Assert: len is non-negative.
  2. If len is an integer number of device pixels, do nothing.
  3. If len is greater than zero, but less than 1 device pixel, round len up to 1 device pixel.
  4. If len is 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:

  1. puppeteer-script.js
  2. package.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

  1. Create a folder anywhere on your computer, and save the attached puppeteer-script.js in that folder.

  2. Install the latest stable version of Node.js by using the prebuilt installer found here. No changes beyond the default installation are necessary.

  3. Open the command line interface by typing cmd or Command Prompt in your Windows Start menu/search field.

  4. In the command line interface, verify that Node has been globally installed by typing node -v and pressing enter. Then, type and enter npm -v (npm is the Node Package Manager). Both commands should display the version number of the installed programs.

  5. 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/Test to navigate there.

  6. Once you've navigated to the folder where puppeteer-script.js is saved, type and enter the command npm install puppeteer to install the latest version of Puppeteer. Once the installation is complete, the folder should have a few new items: a node_modules folder, a package.json file, and a package-lock.json file.

  7. Update your package.json file to look like the version I've included here by adding "type": "module", to the JSON object. This will make it so you can call import puppeteer from 'puppeteer' in the script.

  8. In the command line interface (still within the folder you created in step 1), type node puppeteer-script.js to 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions