w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 666 forks source link

[css-env-2] env(scrollbar-inline-size) #4691

Open argyleink opened 4 years ago

argyleink commented 4 years ago

Scrollbars are a system provided, dynamic, and out-of-developer's control component that can "get in the way" by consuming unpredictable amounts of space. It'd be nice if the platform provided an environment variable that held the contextual system width value for scrollbars so that developer could use calc() to mitigate unpredictability.

I've gently proposed using logical property syntax for the env variable name, since I feel this is the language used to talk about sizing on the web:

env(scrollbar-inline-size)

Here's a great bad example of how folks are working around this today: (aka, negative margins)

var checkScrollBars = function(){
    var b = $('body');
    var normalw = 0;
    var scrollw = 0;
    if(b.prop('scrollHeight')>b.height()){
        normalw = window.innerWidth;
        scrollw = normalw - b.width();
        $('#container').css({marginRight:'-'+scrollw+'px'});
    }
}

All in all, having access to the scrollbar width would give folks the values they need to gracefully handle the dynamic aspects of scrollbars with just CSS.

I assume there's aspects to this work that overlap with custom scrollbars, so this env var would need to be a property that can update at runtime yeah? Help me think out the rest of the fringe edge cases, as well as use cases where scrollbar width would be super helpful in your layouts/ui. Thanks!

jensimmons commented 4 years ago

This is related to https://github.com/w3c/csswg-drafts/issues/4329.

Basically, whatever solutions we come up with in the vertical direction will also be applied in the horizontal direction.

We discussed these issues today at the CSSWG face-to-face in A Coruña. Scrollbars are similar to keyboards. And yes, we were settling on env() variables for the dynamically changing measurements (and not another option). There are pros & cons, usecases & objections.

emilio commented 4 years ago

The scrollbar size can be controlled by the user, so probably should be native-scrollbar-inline-size or something like that.

I presume this is not meant to be element-dependent, right? env(native-scrollbar-inline-size) would be the same regardless of the element having scrollbars or not... right?

cbiesinger commented 4 years ago

This was also discussed today at the CSS F2F as part of https://github.com/w3c/csswg-drafts/issues/4674

jonjohnjohnson commented 4 years ago

Something like env(scrollbar-width) that is the initial length or length computed from the auto value of the scrollbar-width property?

https://github.com/w3c/csswg-drafts/issues/2630#issuecomment-387909043

fantasai commented 4 years ago

Relevant F2F minutes: https://lists.w3.org/Archives/Public/www-style/2020Feb/0007.html & https://lists.w3.org/Archives/Public/www-style/2020Feb/0011.html

bramus commented 3 years ago

With [css-overflow-4] scrollbar-gutter making it possible to prevent unwanted Layout Shifts caused by scrollbars, what does the future of env(scrollbar-inline-size) look like?

I mean, getting the size of the scrollbar is handy, but as we can achieve the same behavior with scrollbar-gutter: stable both-edges;, do we still need env(scrollbar-inline-size)?

/* Keep content centered by manually offsetting the padding-inline-start */
.keep-content-centered-using-envvar {
  padding-inline-start: env(scrollbar-inline-size);
}

/* Keep content centered using scrollbar-gutter */
.keep-content-centered-using-scrollbar-gutter {
  scrollbar-gutter: stable both-edges;
}

Or are there any other use-cases for env(scrollbar-inline-size) besides keeping things visually centered?


Also note that manually taking env(scrollbar-inline-size) into account can reproduce funky results when combined with scrollbar-gutter: stable both-edges;:

/* This looks is a bad combination to do, as you'll end up with a double gap at the edge opposing the scrollbar */
.bad-combination {
  padding-inline-start: env(scrollbar-inline-size);
  scrollbar-gutter: stable both-edges;
}
Schepp commented 2 years ago

I still have a use-case and that is when making elements horizontally break out from their parent elements, extending them back to the full available viewable area + combined with programmatic scrolling.

This is the pretty much standardized™ example code to make child elements extend back to full width, regardless of their parent's width (imagine a hero image in an article):

:root {
  overflow-x: hidden;
  overflow-y: scroll; /* could also be done with scroll-gutter */
}

.outer {
  width: 100%;
  max-width: 40rem;
  margin: 0 auto;
}

.inner {
  width: 100vw;
  margin-inline: calc((100vw - 100%) / -2);
}

To my knowledge, this is still the way to go to do these types of things when you work with components and you don't know in what type of element the .inner element will end up living (the alternative approaches using CSS Grid don't work well with componentization or rely on subgrid).

But .inner element will now extend below the scrollbar of the root, creating horizontal overflow. This can again be countered by setting overflow-x: hidden on that root. Problem is, you can still scroll programmatically. This gets apparent, once you start scrolling things like sliders or galleries via scrollIntoView() from left to right and then back again.

Here is a demo of that behavior: https://codepen.io/Schepp/full/jOYdQbv Note how the page jumps horizontally every time you click on "< start" and "end >" in the slider controls (except for the very first time)

Trying to contain scrolling with overscroll-behavior: contain; doens't work either, as this works from inside out, but scrollIntoView() approaches the task from outside in.

Having env(scrollbar-inline-size) available would enable us to fix that unwanted (programmatic) scrolling by changing our code as follows:

.inner {
  --viewable-area: calc(100vw - env(scrollbar-inline-size));
  width: var(--viewable-area, 100vw);
  margin-inline: calc((var(--viewable-area, 100vw) - 100%) / -2);
}

PS: In theory, one could also trigger page jumping by focusing something close to the right border of the page, and then again something close to the opposing border.

Schepp commented 2 years ago

Okay, so the so far unknown to me overflow: clip seems to be the solution to all my problems 🥳

If you apply this to e.g. <body>, instead of overflow: hidden, it stops programmatic horizontal scrolling of it, when children extend beyond its width (being 100vw - scrollbars):

body {
  width: 100%;
  overflow: clip;
} 
LLyaudet commented 1 year ago

Hello, another use case is to to have a position: fixed div stuck on the right of the screen cleanly. Many people use JS workarounds right now for many use cases. See for example https://stackoverflow.com/questions/28360978/css-how-to-get-browser-scrollbar-width-for-hover-overflow-auto-nice-margi The best option in my opinion would be to have parent-scrollbar-width variable accessible to calc.

simevidas commented 1 year ago

@Schepp Re --viewable-area: calc(100vw - env(scrollbar-inline-size)), if the page does not have a classic scrollbar, would you want the .inner element to extend all the way to the right edge of the browser?

If yes, then env(scrollbar-inline-size) would not be the solution, since it would be a fixed length, regardless of whether the page actually has a classic scrollbar or not. So --viewable-area would be a fixed length as well, and on a page without a classic scrollbar, that length would be smaller than the viewport width.

Schepp commented 1 year ago

@simevidas yes, I would want the element to then take up the full space up to the right edge of the browser.

I think what I mean with scrollbar size is how much in-flow space it takes up. A lot of scrollbars just overlay the underlying content, only appearing when triggered, and would then result in the env having the value 0.

Here is the code I currently have to use to find out myself and fix my problems via custom property --scrollbar-inline-size:

// the following gets all executed right after opening <body> tag
const elem = document.createElement('div');
const referenceElem = document.createElement('div');
const measureElem = document.createElement('div');

Object.assign(elem.style, {
  position: 'absolute',
  width: '100%',
  visibility: 'hidden',
});
Object.assign(referenceElem.style, {
  overflow: 'scroll',
  width: '50px',
});

const determineWidth = () => {
  const scrollbarInlineSize = 50 - measureElem.offsetWidth;

  window.sessionStorage.set('scrollbar-inline-size', scrollbarInlineSize);
  document.documentElement.style.setProperty('--scrollbar-inline-size', `${scrollbarInlineSize}px`);
};

elem.appendChild(referenceElem);
referenceElem.appendChild(measureElem);
document.body.appendChild(elem);

const scrollbarInlineSize = window.sessionStorage.get('scrollbar-inline-size');

if (scrollbarInlineSize !== null) {
  document.documentElement.style.setProperty('--scrollbar-inline-size', `${scrollbarInlineSize}px`);
} else {
  window.requestAnimationFrame(() => determineWidth);
}

(imagine a little more complexity to it and this being the gist)

kizu commented 1 year ago

Would copy my comment from https://github.com/w3c/csswg-drafts/issues/6026, as it fits this issue more:

In our product we're currently essentially calculating the scrollbar-inline-size via JS, creating an invisible block with an overflow, and retrieving its width if it has one, then storing it as a custom property on root, to be used in calculations.

Being able to use a keyword for this, alongside a query to detect if scrollbars are present on a container would be very helpful.

Why I'm mentioning “keyword” and “container” rather than an “environment variable” and “media query”: we have scrollbar-width which can control if we have the scrollbar, and we can have a container that can be scrolled programmatically when it has overflow: hidden. That means that authors could want to a) detect if a certain scrollport has a visible scrollbar, and b) get the width of a scrollbar in that case, which can be different for different containers (scrollbar-width and also current webkit scrollbar pseudo-elements).

simevidas commented 1 year ago

There are two separate problems:

  1. How much space do scrollbars consume in the browser, generally speaking? In browsers with overlay scrollbars, that’s 0px (because the scrollbars are overlaid). In browsers with classic scrollbars, that’s by default 15px on macOS and 17px on Windows. It could be more or less, depending on other factors.

  2. Is a vertical classic scrollbar currently present on the web page?

@Schepp’s function answers the first question. The proposed env(scrollbar-inline-size) variable that’s the topic of this issue would also answer this question, if I understand correctly.

That’s good, but I’m also interested in the second question. Being able to answer this question in CSS would allow developers to solve the 100vw problem.

As you can see, developers cannot solve the 100vw problem with env(scrollbar-inline-size) alone, and without knowing the answer to the second question.

It does not look like the second question will be answered in CSS anytime soon (see https://github.com/w3c/csswg-drafts/issues/6026#issuecomment-1713253071). I think the best bet to make 100vw less of a problem in browsers with classic scrollbars is to make it smaller when scrollbar-gutter: stable or overflow-y: scroll is set on <html>. That proposal is discussed in https://github.com/w3c/csswg-drafts/issues/5254.

Schepp commented 1 year ago

It does not look like the second question will be answered in CSS anytime soon

Well, maybe it will, as people in the Chrome/ium team are experimenting with State Queries, which are meant to reflect exactly these types of things.

bramus commented 1 year ago

Dropping this here, which was proposed at this year‘s F2F in Cupertino: https://gist.github.com/bramus/bcca5788d8ced82837180b7a15760c84

Essentially you need 3 things:

kizu commented 1 year ago

As a sidenote, with an @property and container query length units we can kinda work around the issue like this: https://codepen.io/kizu/pen/WNLjJvq — would currently work in Chrome and Safari. Won't work in Firefox when there is another container around the element which we want to apply our --vw to, but can work everywhere if we're sure the topmost wrapper would be the only container.

But yes, this requires an additional wrapper around everything, and is more cumbersome. But can be a good viable workaround as soon as @property would land in Firefox, and this could be used for any scrollable containers, not only for the viewport, though would require two containers to set it up.

(posting as a way to demonstrate a future workaround, and as something people could use for testing how the potential APIs would work in CSS)

lukewarlow commented 1 year ago

Given scrollbar-width can change the scrollbar width on a per element basis I don't think env(scrollbar-inline-size) being a static value from the engine (e.g. 15px) would actually solve the issues.

I think the main one is that 100vw should just account for scrollbar width. That seems to be the primary case where people need to account for scrollbar width. Though I understand that could cause a cycle which seems to be the reason against it?

bramus commented 1 year ago

Given scrollbar-width can change the scrollbar width on a per element basis I don't think env(scrollbar-inline-size) being a static value from the engine (e.g. 15px) would actually solve the issues.

Maybe a second env var that exposes the thin size – e.g. thin-scrollbar-inline-size – would help here? If authors have set scrollbar-width: thin;, then they should use env(thin-scrollbar-inline-size) in their calculations. Otherwise (in case of scrollbar-width: auto;), they should use env(scrollbar-inline-size)

lukewarlow commented 1 year ago

Would that rely on knowing the current value of the property? Style container queries feel a bit ott for that but perhaps it's fine?

brunoais commented 1 year ago

I really do think that adding another env like that is complicating, specially if you consider the interest in having the least edits every time to get a change is done. Or worse, to do it right, with feature detection, you must actually check if the browser supports it and have an alternative for it... What a complete mess that would be...

SebastianZ commented 12 months ago

Given scrollbar-width can change the scrollbar width on a per element basis I don't think env(scrollbar-inline-size) being a static value from the engine (e.g. 15px) would actually solve the issues.

Maybe a second env var that exposes the thin size – e.g. thin-scrollbar-inline-size – would help here? If authors have set scrollbar-width: thin;, then they should use env(thin-scrollbar-inline-size) in their calculations. Otherwise (in case of scrollbar-width: auto;), they should use env(scrollbar-inline-size)

I believe a proper solution needs to be context aware. So basically the opposite of what @emilio wrote earlier because use cases want to consider the actual scrollbar width in their calculations. With a contextual value, authors don't have to care about whether there are normal or thin scrollbars or no scrollbars at all. That means, there should only be one keyword that refers to the width of the scrollbars of the nearest scroll container.

Sebastian