Skip to content

[css-borders] Add a 'hairline' border-width value #3720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
tabatkins opened this issue Mar 11, 2019 · 84 comments
Open

[css-borders] Add a 'hairline' border-width value #3720

tabatkins opened this issue Mar 11, 2019 · 84 comments

Comments

@tabatkins
Copy link
Member

tabatkins commented Mar 11, 2019

People regularly ask for the ability to size things in device pixels (#3715 is the latest). This is virtually always intended for the use-case of sizing borders, to create "hairline" borders or separators.

Since adding an actual device pixel unit is out of the question, we should still address this use-case directly, and add a hairline keyword to 'border-width', representing "the thinnest width that the UA believes is recognizable, snapped to the nearest whole number of device pixels". Right now, that'll be 1 device pixel, but if screens get significantly higher-res, it'll probably resolve to whatever number of device pixels gives you a value between 1/3 and 1/2 of a CSS px.


Why can't we add a device pixel unit, so you could just say border-width: 1dp?

Because device pixels are unpredictable in size. If you try to size anything bigger than a hairline with device pixels, the size will, today, vary by more than 300%, as there are devices that have 1dp==1px, and devices with 3dp=1px, and I think some devices are pushing higher than that. As display technology gets better, this gulf will continue to grow, as there will likely always be 1dp==1px devices in use.

We already know from past experience that authors can't generally handle units with an unpredictable "sliding scale" ratio; the CSS2 spec used to have in and px decoupled, so that in was as close to an actual inch as the browser could get, and px was the nearest whole number of device pixels approximating the visual angle of a device pixel on a 96dpi monitor. However, so many authors used the physical units (pt/in/cm/mm) to size their pages while assuming 96px=1in that browsers implementing the spec got broken pages on devices whose in-px ratio was different. And that was a ratio that could differ whose min and max could differ by, at most, 50%! (Handling an unpredictable ratio is a fundamentally very difficult problem! We have better, less fragile layout tools to help with that these days, but it's still virtually guaranteed to result in problems if we expose this.)

Plus, the notion of a "device pixel" is screen-specific; when printing, the analogue to "device pixel" is the "dot", the smallest drop of ink that can be placed at a particular location. Printers not only have much smaller dots than screens have pixels, and in a larger range (consumer printers can vary from 300dpi to more than 1000dpi), they also vary based on color; black-and-white dpi is often much higher than color dpi. Sizing anything based on "device pixels" will either be wildly unpredictable on printers, or else it'll be a "virtual device pixel" that doesn't correspond to anything physical anyway.

And all this isn't even getting into screens that simply more exotic than the "rectilinear grid of hardware pixels" design that this sort of feature assumes. Some screens layer their pixels in a non-square grid. Some have differently-sized pixels, layered in separate rectilinear grids. Some have different grids for each subpixel color. All in all, the notion of a "device pixel" as something you can naively size things in is already nonsensical for many output devices, and the situation is likely to only get worse as time goes on.

Why not a device-pixel rounding function?

In #3715 @florian and I talk about a function that rounds a length to the nearest number of device pixels, as a compromise that delivers many of the benefits of device-pixel sizing without as many of the downsides. On further consideration, I don't think we want this either.

For the case of hairline borders, a device-rounding function achieves basically the same thing as a 'hairline' value, just in a slightly more complicated way. Users have to know to write border-width: dp-round(.5px) or what-have-you, rather than border-width: hairline;. The only theoretical benefit of dp-round() over hairline is that it lets you distinguish between whether you really want .33px or .5px, when that distinction might matter for visibility, but in practice that's a wash anyway, since a 3x screen will still probably choose .33px over .67px if you say dp-round(.5px), and a 2x screen will still use .5px if you say dp-round(.33px), and many devices will end up still choosing 1px anyway, so the theoretical gain in precision is lost in practice. In a theoretical future with super high-res screens, it might become relevant; we can revisit the question at that point. ^_^

For any other case, rounding to the device pixel doesn't do anything meaningful unless you're also rounding the position. Otherwise it's possible that the box is exactly 200dp tall, but it's offset 150.2dp from the top of the screen, so it's still fuzzy. And unless we offer position-rounding as some sort of separate property switch, you have to instead use dp-round() on your ancestors, too, all the way up to the root. (And depending on how you're being laid out, that still might not be enough; flexbox alignment has no way to force a rounding on the gap between flex items, for example.)

So, dp-round() is overkill complexity for hairlines, and doesn't do anything useful for most non-hairline cases.

@AmeliaBR
Copy link
Contributor

I think this is a good compromise to the issue, especially if there is spec text recommending that browsers consider accessibility when defining the "hairline" thickness. E.g., if I'm browsing the web with 400% zoom level, I probably don't want lines that are only one retina-screen-pixel thick!

For the more general case of "snapping" lines to an even number of device pixels, the SVG shape-rendering property is another approach, which could be generalized to apply to CSS borders and boxes. However, shape-rendering: crispEdges wouldn't remove the need for a dedicated hairline option, since snapping very fine lines to the nearest number of device pixels sometimes results in snapping them down to nothing!

@tabatkins
Copy link
Member Author

Yes, the spec should definitely state that the hairline-ness should be taken at "normal" zoom levels; zooming in shouldn't keep them hairline!

@dbaron
Copy link
Member

dbaron commented Mar 11, 2019

It seems like at some point these borders might become thinner than the user of this keyword expects. For example, when printing, or as devices get even-higher-resolution.

Also... with Gecko's border rounding behavior (round down in device pixels, except round values between 0 and 1 device pixels up to 1) you can effectively do this today without facing that problem. For example, a border with width `0.25px' will be one device pixel, unless there are 8 or more device pixels per CSS pixel. I think there was some movement of other browsers towards that behavior, but I'm not sure if things have converged.

@tabatkins
Copy link
Member Author

@dbaron Did you miss this part (quoted below), or is your comment asserting that something about it would still be problematic?

Right now, that'll be 1 device pixel, but if screens get significantly higher-res, it'll probably resolve to whatever number of device pixels gives you a value between 1/3 and 1/2 of a CSS px.


Also... with Gecko's border rounding behavior [snip]

Hm, interesting. http://software.hixie.ch/utilities/js/live-dom-viewer/saved/6749 tests this behavior. In Chrome, on my 1x desktop monitor, I get a 1px border for everything down to .05px; the .01px border disappears. That might be an internal rounding issue?

There's an additional weirdness in Chrome, tho - despite the border displaying the same size in all of them, the internals of the box itself lays out as different heights, despite the only thing giving it a height being a padding: 2px declaration! Looks like the 1px-border is 4px tall internally, the .5px and .33px are 3px tall, and the .1px is 2px tall. It looks like we're rendering the border-box's height "correctly" for the specified padding+borders (6px, ~5px, and ~4px, respectively), but then just painting the borders larger, so it overlaps the padding box internals?

It looks like Firefox correctly renders a 4px padding box on each of them, so I assume this is a Chrome bug in the timing of our border-width snapping vs other layout calculations.

@tabatkins
Copy link
Member Author

(Confirmed with @atotic, this is a Chrome bug that'll be fixed when they get around to rewriting the relevant chunk of our layout code into the new LayoutNG form.)

@dbaron
Copy link
Member

dbaron commented Mar 11, 2019

I did miss that "Right now" part -- although I think the proposal needs to define that behavior today, for printing.

@jonjohnjohnson
Copy link

jonjohnjohnson commented Mar 12, 2019

When I don't want a keyline to effect geometry, I've used box-shadow. I don't s'pose we'd also be able to solve this hairline value for properties like box-shadow & outline-width, let alone hopeful properties like text-stroke-width(compat) or text-shadow?

@tabatkins
Copy link
Member Author

I did miss that "Right now" part -- although I think the proposal needs to define that behavior today, for printing.

Right, sorry for not being clear, it is absolutely intended that the spec will explicitly say that UAs should choose an appropriate small width, and recommend that it probably be no smaller than 1/3 or 1/2 of a px.

@tabatkins
Copy link
Member Author

I don't s'pose we'd also be able to solve this hairline value for properties like box-shadow & outline-width, let alone hopeful properties like text-stroke-width(compat) or text-shadow?

Hm, maybe.

Tho at this point dbaron's statement about just relying on rounding behavior (and specifying that you must always round away from zero) is starting to sound really good.

I had a chat with @bfgeek about this tho, and he has some reservations about it. He could elaborate on this more; I don't fully understand the problems he was alluding to.

@cncuckoo
Copy link

cncuckoo commented Mar 14, 2019

I do think the supporting for fractional order px value, means 0.1~0.9px (may be even more precise than that in the number of both decimal places and decimal digits), is more useful than we finally only can have a new hairline key word value for border-width property.

As on mobile Apps, Android can use 0.3dp or 0.5dp , and iOS can use 0.3pt or 0.5pt to express the hairline line. For CSS, px just plays the same role as dp on Android and pt on iOS.

For the web pages that is embed in mobile apps, I wish, one day, we can use 0.3px or 0.5px to match the exact border width with the mobile apps, so the users can see the same thick or thin lines on their Apps.

@AmeliaBR
Copy link
Contributor

I don't s'pose we'd also be able to solve this hairline value for properties like box-shadow & outline-width, let alone hopeful properties like text-stroke-width(compat) or text-shadow?

The outline-width property references the compound type that is defined for border-width and therefore accepts border-width keywords. So it should automatically also get a hairline value if it is added as a new keyword.

stroke-width as defined in SVG doesn't accept keyword values, but there's no reason we couldn't add them (meaning, thick, medium, thin, as well as hairline) for the new module that extends it to CSS text.

box-shadow and text-shadow would be more complicated, since there are many different lengths (offset and expansion/inset) interacting. But maybe it's worth a separate issue to discuss the expected behavior of fine resolution shadow effects — I recently discovered that browsers are very inconsistent in how they handle text-shadow offsets of less than 1px (see Twitter thread with people posting screenshots).

@AmeliaBR
Copy link
Contributor

I do think the supporting for fractional order px value … is more useful than we finally only can have a new hairline key word value

There's nothing stopping you from using a fractional px value for border width in CSS — it's just not guaranteed to snap to an even number of device pixels, and will therefore look bad on a screen with a different resolution than you designed it for. If this is really important, however, you could use media queries to check for the screen resolution.

The benefit of the keyword is that the browser would figure out the exact value required to get a crisp line.

@bradkemper
Copy link
Contributor

There's nothing stopping you from using a fractional px value for border width in CSS — it's just not guaranteed to snap to an even number of device pixels

I think it should. If it was only a 1x screen, then use opacity to simulate the thinner line, like is typically done for antialiasing scaled images. So a 0.5px line is rendered with 1px at half the opacity of what it normally would be. On a 3x screen, it would use 2 device pixels, at 0.75 opacity.

@tabatkins
Copy link
Member Author

There's nothing stopping you from using a fractional px value for border width in CSS — it's just not guaranteed to snap to an even number of device pixels,

It actually is guaranteed to do that. CSS itself doesn't guarantee it, but all browsers snap borders to whole device pixels, precisely because they otherwise look bad. (And do some fancy rounding to make sure all borders of a given width round in the same direction.)

@bradkemper
Copy link
Contributor

bradkemper commented Mar 15, 2019

@tabatkins I thought Chrome and Safari (and maybe others) snapped to the whole px, not to the device pixel smaller than 1px. Is that no longer true?

@bradkemper
Copy link
Contributor

At least for border widths, I mean. Things like centering might be on device pixel space.

@tabatkins
Copy link
Member Author

No, they definitely do device pixel snapping. Confirmed by looking at http://software.hixie.ch/utilities/js/live-dom-viewer/saved/6749 on my high-dpi phone; the first box (border-width: 1px) definitely has wider borders than all the others.

@bradkemper
Copy link
Contributor

Hmm. You're right. But they all (except 1px as and 0px) seem to be rounded to the same value on my iPhone X. Looks like maybe 0.25px. Maybe we can standardize and improve this then? It seems better than 'hairline' to me. But maybe both are useful.

@bradkemper
Copy link
Contributor

On my retina desktop display (Mac), in Chrome for Mac, the 0.05px to 1px borders all look the same (1px), and the last 2 are rounded down to zero.

@cncuckoo
Copy link

If this is really important, however, you could use media queries to check for the screen resolution.

@AmeliaBR Yes, I could. But when I viewed http://software.hixie.ch/utilities/js/live-dom-viewer/?saved=6749 on Chrome/Safari/Firefox/Opera Mini on my iPhone XS Max, as @bradkemper has pointed, all values that less than 1px are rounded to 0.25/0.3px (maybe 1 device pixel). Based on this, I think using media queries to check the screen resolution is also not useful. Because it seems
none of the browsers have yield the control to us for how many device pixel one fractional px value could be snapped to.

@SelenIT
Copy link
Collaborator

SelenIT commented Mar 18, 2019

It seems that different properties are snapped to device pixels by different rules. Can anybody shed some light on the general logic behind this, please?

@FremyCompany
Copy link
Contributor

My two cents here, maybe we should instead have a border-style: hairline and outline-style: hairline that would draw a perfect border around the content inside, and potentially leave extra blank space when that size is smaller than the border-width.

That way, the layout doesn't become unstable when you zoom in, only the rendering can change.

@FremyCompany
Copy link
Contributor

I'm personally mostly concerned about layout changing as you zoom, this could have unintended consequences.

@bradkemper
Copy link
Contributor

@FremyCompany zooming should still scale it, along with everything else.

@FremyCompany
Copy link
Contributor

FremyCompany commented Mar 22, 2019

@bradkemper Wait, so when you zoom, the "hairline" stops being an hairline, did I get that right?

Because for instance if you had specified 1hairline, landed on 1dpx, then zoom 1.5x then the new border size won't be a round number of pixels (1.5dpx in this case) and won't be an hairline. I guess that this is an option, but that doesn't sound great to me.

My proposal would have been to have the border be 1px all the time (which might be 1.5dpx, whatever), but then draw only 1dpx/2dpx around the content at painting time, and stick to either 1dpx (or 2dpx etc...) as you zoom while continue to scale the 1px value normally.

@bradkemper
Copy link
Contributor

@FremyCompany

Yes, Tab replied to Amelia’s comment with this:

Yes, the spec should definitely state that the hairline-ness should be taken at "normal" zoom levels; zooming in shouldn't keep them hairline!

I take zooming to be like looking through a magnifying glass, where a hair (or hairline) looks bigger and thicker as a result.

@bradkemper
Copy link
Contributor

Because for instance if you had specified 1hairline, landed on 1dpx, then zoom 1.5x then the new border size won't be a round number of pixels (1.5dpx in this case) and won't be an hairline. I guess that this is an option, but that doesn't sound great to me.

That’s no worse than zooming in 1.5x on 1px line when 1dpx = 1px (non-retina display), is it?

@FremyCompany
Copy link
Contributor

Because for instance if you had specified 1hairline, landed on 1dpx, then zoom 1.5x then the new border size won't be a round number of pixels (1.5dpx in this case) and won't be an hairline. I guess that this is an option, but that doesn't sound great to me.

That’s no worse than zooming in 1.5x on 1px line when 1dpx = 1px (non-retina display), is it?

Fair point.

@Loirooriol
Copy link
Contributor

represents some fraction of a pixel

Is this fraction left up to the UA to decide?

  • If not, e.g. we define that hairline means border-round(0.1px), then it seems like we could just avoid adding hairline and let authors use --hairline: border-round(0.1px) if they want.
  • If yes, why only add hairline to a few properties? Why not make it a unit?

@edokeh
Copy link

edokeh commented Apr 10, 2025

On Safari, 0.1px or 0.2px borders may not render.

See this bug https://bugs.webkit.org/show_bug.cgi?id=286256

@SebastianZ
Copy link
Contributor

As we consider to expose device pixel rounding as a function, I think it makes more sense to call it something related to that like round-to-device-pixels() (possibly something shorter) instead of border-round().
Also, the algorithm to "snap a length as a border width" should be renamed accordingly.

Sebastian

@bernhardf-ro
Copy link

For both hairline and the rounding function (implicit or explicit) it would be great if output media with no device pixel resolution, e.g. PDF, would be covered by the specification as well.
The primary question is whether an infinite resolution should be used, making rounding pass through the input value unmodified, or a fixed resolution should be applied, e.g. 2*96dpi, 4*96dpi, 4*72dpi, 300dpi.

@webstrand
Copy link

@SebastianZ why not reuse the existing round(up, 0.5px, 1dpx) function? Then you can reuse the existing rounding modes provided by that function. Ultimately, if you expose a device-pixel-round() authors will be able to use it to derive a 1dpx unit anyway. You could restrict the unit 1dpx to be a singleton, so 2dpx would be invalid (or extensible in the future), forcing authors to resort to calc(100 * 1dpx) if they actually want to use the unit. This is probably sufficiently discouraging to prevent casual use of the unit?

In my sites I currently inject a:

@property --dpx {
	syntax: "";
	inherits: true;
	initial-value: 1px;
}
:root { --dpx: 0.8666666666666667px }

via javascript so that I can make things look non-fuzzy. My displays have a native non-integer ratio to css pixels, which is why the size is fractional.

Used like round(var(--thumbnail-width), var(--dpx)) it works very well in solving the problem in chrome/firefox.

@tabatkins
Copy link
Member Author

tabatkins commented May 2, 2025

As I've said a few times in this thread, we still don't want to actually tie anything to "1 device pixel". That unit of measure is not guaranteed to be reasonable in the future. We just want "a UA-defined 'small enough' length, greater than zero and less than or equal to 1px, and likely an integer multiple of the device pixel size".

@webstrand
Copy link

webstrand commented May 5, 2025

Device pixels may not be a reasonable for rendering border-widths, but if there's a smaller addressable unit than 1hairline you get things like css grid distributing space in surprising ways (for example https://codepen.io/webstrand/pen/MYYGqWQ).

So 1hairline needs to be defined as "smallest layout unit that's an integer multiple of device pixels" so that layouts like flex/grid can't split 1hairline in half and distribute its space between elements. This kind of issue makes putting borders around grid cells tricky without 1dpx, because they're happy to layout elements with sub-pixel gaps of whitespace.

Here's a more complex but less contrived example https://codepen.io/webstrand/pen/pvvVxgY. Recommended viewing with zoom (not on safari) or on a display with a non-integer devicePixelRatio. The checkboxes toggle the correction for alignment to device pixels and css pixels.

@Loirooriol
Copy link
Contributor

@tabatkins You have explained the problems with having a "1 device pixel" unit.

But I think you haven't explained the reasons for not making hairline a unit. #3720 (comment)

@tabatkins
Copy link
Member Author

Yeah, I think a unit would also be acceptable, it's just slightly weirder. But I suppose it does work better as a unit when you're doing anything non-trivial, like a calc().

@Loirooriol
Copy link
Contributor

Then it seems simpler to me to just add hairline as a unit, and not add border-round()since it can be expressed in terms of standard math functions and 1hairline. But no strong opinion.

@smfr
Copy link
Contributor

smfr commented May 6, 2025

But what is 100hairline?

@tabatkins
Copy link
Member Author

100 times the hairline width? I'm not sure I understand the intent of your question.

@smfr
Copy link
Contributor

smfr commented May 6, 2025

I was suggesting that a hairline unit doesn't make much sense when used with values other than 1; 100hairline is rather nonsensical.

@tabatkins
Copy link
Member Author

100hairline might be weird, but calc(100px + 2hairline) (to match the size of another element with width: 100px; border: 1hairline solid black;) seems perfectly reasonable.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-borders] Add a 'hairline' border-width value.

The full IRC log of that discussion TabAtkins: Discussed border-rounding behavior, where borders with nonzero width but less than a pixel will round to something visible
TabAtkins: lot of discussion
TabAtkins: conversation seemed to lean towards exposing a 'hairline' keyword for a few propoerties
TabAtkins: and maybe a rounding function that rounds like borders do, always away from zero
TabAtkins: since then, Oriol suggested 'hairline' as a unit
TabAtkins: maps to a number of device pixels
TabAtkins: can be used as a length unit anywhere lenghts are allowed
TabAtkins: avoids need for new rounding function
TabAtkins: also useful if you want another box to match the width of a box with hairline border
TabAtkins: could specify calc(100px + 2hairline)
TabAtkins: smfr concerned that larger multiples wouldn't be reasonable
TabAtkins: but use cases for small multiples
q+
TabAtkins: so I suggest resolving on this approach
TabAtkins: a new unit that is 1px or less, but at least one device pixel
ack emilio
q+
emilio: one question, should we make it a keyword rather than a unit?
emilio: a unit that's such a mouthfull feels weird
q+
q+
TabAtkins: A keyword either is not usable in calc, or is one of hte calc keywords, unless we do something to make it usable both ways
smfr: i wo9uldn't object to the unit, think ti's a bit weird tho
TabAtkins: Exactly what we do today with border witdths less than a pixel. We still have some def of what it means internally
q-
fantasai: i think making this a unit feels wrong, if you want the boxes to match you should use box-sizing
ack smfr
ack fantasai
fantasai++
fantasai: people might start using the unit to reference device pixels in a way that's more fragile than we want
flackr: if an author wants something thicker than a hairline...?
TabAtkins: max(1hairline, .3px)
ack flackr
ack fantasai
fantasai: i think ahving a rounding function makes more sense
fwiw, I think implementing it as a keyword seems trivial-ish as well (so that it works in calc() and other s too)
TabAtkins: well this clearly wasn't doable in five minutes
I have concerns about device printing with a unit that yields a number of device pixels.
s/printing/fingerprinting/
emeyer, device pixel ratio is already exposed in numerous other ways

@astearns astearns removed the Agenda+ label May 7, 2025
@kizu
Copy link
Member

kizu commented May 7, 2025

I wonder if it could be a keyword similar to pi or e?

And additionally allow it without calc() around it for length? (adding to the syntax, I guess?)

Then, both border: hairline solid; will make sense, and you could use it in a calc() like calc(100px + 2 * hairline)?

@Crissov
Copy link
Contributor

Crissov commented May 7, 2025

hairline is acceptable as a border width keyword or environment variable, but feels icky as a unit, even if shortened to hl. I would suggest dot in that case, but I think enough arguments have been provided against this solution.

I had also considered constants like pi earlier today, but they are specifically numeric and their value must not depend on implementation or device. That is what env() is for – isn’t it?

@tabatkins
Copy link
Member Author

tabatkins commented May 8, 2025

So, here are the options:

  1. Add a hairline keyword (so border-width: hairline works), and a hairline calc-keyword (so width: calc(100px + 2 * hairline) works).
  2. Add a hairline unit (so border-width: 1hairline works, as does width: calc(100px + 2hairline))
  3. Add a function (so border-width: border-round(0.1px) works, as does width: calc(100px + 2 * border-round(0.1px)))

2 and 3 both have the advantage that they work everywhere, automatically, with no grammatical ambiguity. We don't have to review any existing properties, or keep this in mind with any future properties.

1 doesn't have that advantage. The plain keyword is grammatically ambiguous anywhere a length is mixed with an open set of keywords. We mostly nowadays make sure that open sets of keywords use dashed-idents, but it's still not 100%, and there are several legacy uses that might be ambiguous now. (I haven't surveyed, I'm going on intuition.) But it is certainly the simplest, best-looking of the options otherwise.


Some responses from the late minutes that I couldn't get to during the call:

fantasai: i think making this a unit feels wrong, if you want the boxes to match you should use box-sizing

That only works sometimes, if you can make sure that the box you're matching uses a specifiable border-box size. The example I was giving doesn't allow that - the content box is specifiable (100px), plus two hairline borders, and so the matching box needs to be 100px + two hairlines.

fantasai: people might start using the unit to reference device pixels in a way that's more fragile than we want

I don't see how any of the options are more or less likely to do this. In all of them, you can reference device pixels in some way. border-round(.1px) is exactly the same as 1hairline, just slightly longer to spell, and gives exactly the same abilities.

A very limited solution that just allowed the keyword hairline (and not as a calc keyword, only usable by itself, kinda like an intrinsic size keyword) would indeed make it harder to misuse, but only by making it harder to use in general. And it wouldn't rule it out anyway; you could use multiple elements to achieve the same effects in many situations.

@SelenIT
Copy link
Collaborator

SelenIT commented May 11, 2025

What about making it an env() var, as @Crissov suggested above? It seems to have all the advantages of the option 3 to me 🤔

@tabatkins
Copy link
Member Author

An env() would work, yes - functions are grammatically unambiguous in any location. It's a little long (env(hairline), presumably?), but not too bad.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-borders] Add a 'hairline' border-width value.

The full IRC log of that discussion https://github.com//issues/3720#issuecomment-2864297026
q+
TabAtkins: Options I see here: 1) hairline keyword to border-width and inside calc(), 2) add a hairline unit, 3) add a function, e.g. border-round() or env(hairline)
q+
TabAtkins: benefit of keyword is it's easiest to write
TabAtkins: but downside is grammatical ambiguity means might not be able to use it everywhere easily, need to wrap in calc() some places
TabAtkins: The other two options don't have these downsides
TabAtkins: but several people don't like the usability of the unit, looks weird, I agree
TabAtkins: shortest option but funky
TabAtkins: Function might be the best way to go
env(hairline)
TabAtkins: Question of what is appropriate hairline is an environmental info, so my proposal is env(hairline)
+1 to env(hairline)
Those that do want it as a unit can make one from the env() with the (unimplemented) custom unit feature.
q-
ack emilio
emilio: My concern with env(hairline) is that it behaves differently with CSS zoom
emilio: You multiply by zoom, but as long as less than hairline, would be rounded to device hariline
emilio: whereas if you have a device hairline, get pixels, and zoom the pixels
emilio: zoom could make the hairline inviisble
Is env() allowed anywhere the other options would be?
TabAtkins: zooming down could shrink to invisibility, but could [missed]
q+
emilio: if you had border: env(hairline) and something relies on that value to match, zooming out, the lengths would no longer match because borders are still rounded up
emilio: that's a subtle difference
emilio: considering everything, I think env(hairline) is the best option
scribe+
smfr: are you referring to non-scaling-stroke emilio?
ack smfr
... I don't think we want that
TabAtkins: not the explicit goal but if you have a sufficiently small length it can behave like that
... he's not asking for that as a goal
I wonder if it may make sense to figure out what a hairline should resolve to before we name it. E.g. if it resolves to a device pixel, that is a reasonable unit in itself. If it doesn't resolve to that, then I'm having trouble conceptualizing what it resolves to.
q+
smfr: so if scale() is used it'd get thicker?
TabAtkins: yeah that's not a question
it does not resolve to a specific device pixel, Lea
fantasai: I think I'd prefer a border-round() function
... I'd call it pixel-round()
... I'd prefer to avoid giving the idea that it's possible to do your layout in device pixels
ack fantasai
ack lea
lea: 3 things
lea: not making it a unit makes sense if we only use multiple of one.
lea: env() seems to make sense... is it disallowed anywhere that could otherwise be allowed?
lea: would it make sense to figure out what a hairline resolves to?
lea: when would I use a hairline unit?
lea: I would conceptualize as a device pixel
lea: so as an author, I don't know, and it's too vague
lea: might make sense to figure that out first
+1 lea
or could do an async
TabAtkins: we keep running out of time, so let's do this first next week
lea: Use cases for multiples if you need to match another element to width of element with hairline borders, might need 2x
lea: env() is allowed everywhere
s/lea/Tab/
s/lea/Tab/
TabAtkins: finally, what does it resolve to? It will resolve to some multiple of a device pixel, not necessarily 1. As screens become higher res, device pixels might be too small to be a reasonable hairline
Meeting closed.

@Loirooriol
Copy link
Contributor

I'm still a bit confused about the difference with 1 device pixel.

Is the idea that https://drafts.csswg.org/css-values-4/#snap-a-length-as-a-border-width will then ensure that non-zero border widths are at least 1 hairline instead of the current 1 device pixel? For values above that, will the rounding be to an integer number of hairlines or still device pixels?

@tabatkins
Copy link
Member Author

The use case is not actually "render a border 1 device pixel wide". If you have a thoeretical future screen with 100dppx, you don't want the border that small - it would be practically invisible. The use-case is "render a border thinner than 1px, if it's possible for the screen to do so while maintaining the sharp line we expect from borders". You can do lines that thin in print, and it's totally reasonable for many cases. So providing a dp unit would just mean that, in a future where screens that high-res exist, we'd have to redefine dp to be a "virtual device pixel", like we do with px` today, and keep it from getting too small, or else sites would end up with effectively invisible UI that the author did not originally intend.

Is the idea that drafts.csswg.org/css-values-4#snap-a-length-as-a-border-width will then ensure that non-zero border widths are at least 1 hairline instead of the current 1 device pixel? For values above that, will the rounding be to an integer number of hairlines or still device pixels?

Min of hairline, increments of device pixels. (There's no guarantee that a hairline width will evenly divide 1px - on a 5dppx screen, a hairline of 2dp might be reasonable, for instance.)

@Loirooriol
Copy link
Contributor

Loirooriol commented May 21, 2025

increments of device pixels

Then this means that border-round(x) can't completely be expressed in terms of env(hairline).

In your example, border-round(0.7px) would round down to 3dp (0.6px) , while round(down, 0.7px, env(hairline)) would round down to 2dp (0.4px).

@benface
Copy link

benface commented May 21, 2025

The use case is not actually "render a border 1 device pixel wide". If you have a thoeretical future screen with 100dppx, you don't want the border that small - it would be practically invisible. The use-case is "render a border thinner than 1px, if it's possible for the screen to do so while maintaining the sharp line we expect from borders".

If that's the use case, isn't it already possible with e.g. 0.5px? I imagine the answer lies in "while maintaining the sharp line we expect from borders", but I don't see what's not sharp about it.

EDIT:

Ah, I think I see the problem.

div {
  --border-width: 0.5px;
  border: var(--border-width) solid black;
  width: calc(100px + 2 * var(--border-width))
}

Presumably, the intention is to make the content's width exactly 100px, but on screens where the 0.5px border-width is rounded to 1px because the physical pixels are too large, it would actually make the content width 101px (the result of calc(100px + 2 * var(--border-width))) minus 2px (the border's width), so 99px.

Is that correct? If so, then either border-round or round(..., ..., env(hairline)) sounds like a good solution to me! In the same example, replacing 0.5px with border-round(0.5px) would make the content width 102px minus 2px, so 100px.

EDIT 2:

Here's an idea, I don't know if it's a good one:

  • 100hl = border-round(1px) = 1px
  • 50hl = border-round(0.5px) = 1px on low density monitors, 0.5px on the first Retina displays, etc.
  • 1hl = border-round(0.01px) = 1 physical pixel (technically could be more, but unlikely to ever be)

(Merged my comments, sorry for spamming)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Thursday morning
Status: Regular agenda
Status: FTF agenda items
Development

No branches or pull requests