Open tabatkins opened 3 years ago
Thank you for filing the issue @tabatkins
While out of scope, I do think it's important to consider web extensions here. Namely, a very natural extension is one that lets you toggle preferences per site and if there's only one thing to set then there's a race between the site and the extension: if they both attempt to override the preference the last one to do so wins.
That will be confusing for users and a perennial source of consternation for the poor maintainer fielding bug reports.
I suppose if the browser could tell whether an extension or a site updated a preference media query then they could use that information to apply precedence but that seems over complicated making it less likely that they're willing to do anything. I'm not sure how they would do that unless CSS is available to web extensions or browsers track scripts injected into sites.
Also unless overriding media queries rolls out with custom media queries how would you feature test this without overrding a preference which could itself mess with such an extension.
Namely, a very natural extension is one that lets you toggle preferences per site and if there's only one thing to set then there's a race between the site and the extension: if they both attempt to override the preference the last one to do so wins.
Sure, but why would you use both a site-based and an extension-based toggle on the same page? One or the other would work fine, so I don't think we need to worry about colliding.
(And it's not like the extension can guarantee things anyway, since the page might use only a manually-set light/dark pref, initialized from the standard MQ.)
Also unless overriding media queries rolls out with custom media queries how would you feature test this without overrding a preference which could itself mess with such an extension.
In this suggestion, overriding is part of custom MQs, so they'd go out together.
oh I somehow missed this issue before 🙈
made a comment on a related chromium bug https://bugs.chromium.org/p/chromium/issues/detail?id=1046660#c46
which guided be back here... (I searched here before - I swear 🙈)
let me copy it in here
Should we expand this issue to also talk about a JavaScript API for setting a "per website preference toggleable on the website itself".
Some ideas are for something in the spirit of
window.setMedia('prefers-color-scheme', 'dark');
- Blog post which problem such a setting would solve https://www.bram.us/2022/05/25/dark-mode-toggles-should-be-a-browser-feature/
- Issue reply with
:media()
is not solving this use case https://github.com/w3c/csswg-drafts/issues/6247#issuecomment-1135531723And to give it some "weight" and backstory I am from the Design System Team from the ING Bank and we struggle to scale dark mode for our design system based on a shared attribute compared to "just" using a
@media (prefers-color-scheme: dark) {
Why? Because aligning different teams in different countries with different knowledge and requirements around web standards is "pretty" straight forward... but aligning a custom attribute (where everyone may have a different idea on where to put it, how to name it) is hard.
PS: if this is not the right place - any pointers?
so it seems here a more appropriate place? 😬
the ideas "sketched out "in both of these links in the quote are very similar to CSS.customMedias.set("prefers-color-scheme", ...);
any way we can help? maybe there is more details needed for the problem space it would solve? I think for the API - there is nothing more to it?
I sort of think that this is all you would need to get a fully custom dark mode that persists while reloading or navigating ... which is like soooo much less than all the other (attribute) workarounds 😅
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dark Mode Switch Example (localstorage for persistence)</title>
<script>
const appColorScheme = localStorage.getItem('my-app-color-scheme');
// restore saved custom color scheme if set
if (appColorScheme) {
CSS.customMedias.set('prefers-color-scheme', appColorScheme);
}
function changeColorScheme(colorScheme) {
localStorage.setItem('my-app-color-scheme', colorScheme);
CSS.customMedias.set('prefers-color-scheme', colorScheme);
}
</script>
<style>
/* light mode is default */
p {
color: black;
background: white;
}
@media (prefers-color-scheme: dark) {
/* dark mode rules */
p {
color: white;
background: black;
}
}
</style>
</head>
<body>
<p>some text</p>
<hr />
<p>Light/Dark preference will be stored for reloads/page navigation</p>
<button onclick="changeColorScheme('light')">go light</button>
<button onclick="changeColorScheme('dark')">go dark</button>
</body>
</html>
If I do CSS.customMedia.set('prefers-color-scheme', 'dark');
will iframes on the page respond to (prefers-color-scheme: dark)
? I would expect them to as you don't want all the page to be dark except a weather widget, and it's no different than a user changing the setting from the iframe's perspective.
But, conversely, I wouldn't expect an iframe to be able to override the containing document's preference.
I guess that depends on the content of the iframe?
image the typical Wordpress theme "browser" where you have the next/previous theme buttons and some info about the theme store at the top and below that you have an iframe of the actual theme... if such a theme comes with its own theme switcher then yes I would expect that iframe to be able to set its own preferred color scheme.
a weather widget will probably not come with its own light/dark mode switch... and even if it does it will hopefully not persist that setting (for example via localstorage)
but yes ultimately how to handle default values is in the hands of the implementer - IMHO rightfully so
My question was how overrides propagate in an arbitrary tree of nested browsing contexts. There are three possibilities:
It could only apply to the context that does the override but then you have a light mode iframe in a dark mode page when it would be homogeneous without an override.
It could propagate to all contexts simultaneously.
It could propagate only to children.
imho it should behave the same as the DevTools switch... e.g. everyone gets the change by default
see example Codepen (https://codepen.io/daKmoR/pen/KKQoQjj?editors=0100) + video
https://user-images.githubusercontent.com/24378/171123032-b0cdff1c-cedb-4d8c-93ea-51dcd77bda1c.mp4
But the iframe content can do as it needs e.g. it could
In any case, if the frame adds code to change the prefers-color-scheme
then it becomes responsible for its value and it can decide when/how/why to sync or not
PS: this assumes prefers-color-scheme
is/will be a document level setting... e.g. not a browser-wide/system-wide setting
PS: this assumes
prefers-color-scheme
is/will be a document level setting... e.g. not a browser-wide/system-wide setting
should this be per-origin?
https://github.com/mozilla/wg-decisions/issues/774 is also related here. In the Firefox UI for example, we set prefers-color-scheme
in the root based on the firefox theme, and it inherits to all frames from there.
Err, meant https://github.com/w3c/csswg-drafts/issues/7213 of course, sorry.
should this be per-origin?
That seems reasonable to me 👍
Yeah, we can't introduce a new communication channel to cross-origin iframes, so the preference can't trickle down everywhere. It def should go down origins that can communicate, tho.
But #7213 does bring up the point that SVG-as-image can't run script or load external resources, so it shouldn't be able to exfiltrate any information. Whatever we do, we should match there.
Is it communications? Is it observably any different from the user popping open OS settings and changing it there?
Yes, because it's the site changing things. It's fine if the user communicates with arbitrary sites, as they can already do that by visiting the site. What we don't want to allow is sites to exfiltrate data to, say, ad exchanges, particularly when we're exploring things like fenced frames to close out the few bits of cross-origin communication left.
I've just come across this issue while doing some research for a proposal I made to WICG. Its a different API shape (not using custom media queries) but the same overall goal. Let developers make use of preference media queries (along with client hints, color-scheme property etc) while also being able to override them. I'd be curious for people's thoughts on my proposal?
That explainer looks pretty reasonable as the way to control preference MQs!
Its a different API shape (not using custom media queries)
The "correct" solution was never intended to involve custom MQs, fwiw, that was just listed as the way authors would be able to work around the lack of a preference override if we never did anything. Overriding preferences directly is definitely better.
I'd be curious for people's thoughts on my proposal?
Overall I think your design looks just fine. Note the discussion in this thread about which descendant frames can see the overridden preference; any cross-origin frame that can communicate with the external world shouldn't see them (and should instead just get the original real user preference). But same-origin frames, and cross-origins that can't communicate, should be able to see the overrides (probably invisibly; that is, they'll just see it as user's preference). I'll open some issues over on your repo.
Btw this was also designed to address this problem and a few more: https://github.com/w3c/csswg-drafts/issues/6247
Though these days we might need a more general :if()
or :condition()
pseudo-class.
:media()
solves the "difficult to style based on both an MQ and a class" problem, but this proposal solves more issues (for the set of the MQs that it's trying to address). The intro to the draft spec has a great explanation of what all it addresses.
The CSS Working Group just discussed [mediaqueries-5] Script control of (prefers-*) queries
, and agreed to the following:
ACTION(fremy): Look at the proposal, see whether there's an issue to be filed
ACTION(TabAtkins): Determine if prefers-reduce-data is handled
Last year I wrote about this problem space and why authors need this on my blog.
To accompany that article, I also built an interactive prototype that shows how it would work:
Try the prototype here: https://codepen.io/bramus/full/yLvbgxL
Notice how everything is nicely in sync:
(This UA provided button could be a setting pane somewhere. I added it to this prominent place for demonstration purposes)
By syncing this setting to the UA’s per-site settings – similiar to how you control access to camera, geolocation, etc – it can persist those settings in your profile and sync it across devices.
As noted in https://github.com/WICG/web-preferences-api/issues/16#issuecomment-1710794442, changing the value from within script should have the proper safeguards. E.g. the UA could ask for confirmation the first time a site wants to change the value, similar to how sharing your geolocation works.
I'll reply to comments inline here.
emilio: I'm not sure I agree overriding things like contrast-preference, reduced-motion, etc. emilio: Those don't seem equally useful. Those are a11y preferences we expose so the page can react to that. There's no use case
I would disagree on the statement that there's no use case. For two reasons, I don't have an issue on the whole with motion but some sites really do overdo it. If they provided a way to disable them per-site (origin) then that would benefit me. Also it's common place for sites specifically ones focussed on accessibility to provide a high contrast theme. Currently this has to be done manually by swapping out the stylesheet. This API proposal would allow for that to just use the media query.
emilio: Some other preferences (reduced-data) affect stuff like what headers the browsers send emilio: there's no mention of that stuff
It is intended that the header reflects the preference. I do mention user preference client hint headers but you are correct I should make it more explicit and include the Save-Data header.
it feels sketchy to override prefers-reduced motion
I'd be keen to here why you think this? If a site want's to be obnoxious and override it to no-preference well they can largely already do this by just ignoring the preference. Excepting the question of UA styles which they don't contro currentlyl so that is a valid concern.
Is there an option to keep this API shape but with reduced preferences? It seems we all agree about colorScheme for example but maybe not some others
Absolutely, I copied all the mq5 preferences by default but I'm more than happy to discuss removal of some. I do think that more than colorScheme are useful though.
There may actually be value for the site to provide a setting that's easier to get to than the OS setting
I think this is a key point I'd like to emphasise. There's some OSs that don't even offer certain preferences, for example Chrome and Firefox for Android only very very recently got the ability to honour a contrast preference. Due to platform limitations I ended up having to implement them using "High Contrast Text" setting which isn't a 1 : 1 match.
It's also worth bearing in mind that there's only 1 platform that allows a "(prefers-contrast: less)" state and that's Windows. Aside from Firefox's forced colors palette in the browser which does offer this capability on other platforms. This API is intended to allow sites to offer a more comprehensive accessibility experience.
If you clear storage will that clear the override?
Yes, if you clear site data the override will be cleared. The exact clearing mechanism is obviously ultimately down to the UA but I don't see this as being much different from offering a reset preferences option like you get with permissions. On top of also being cleared when site data as a whole is cleared.
i don't think we should make it persistent
Why? That's largely the whole basis for the API. We can spec it to have the same bahaviour as any other user controlled storage for UAs that are concerned such as Safari's 7 day storage eviction. But part of the benefit is this could survive that. On the basis it's not really useful for tracking.
If it's not persistent you also get the flash of unstyled content issue if the OS theme and user preference don't match. e.g a flash of white as the page loads and the preference is set.
i think defining exactly who can see this change is important. can UA style sheets see this?
I completely agree and UA styles is a great example of where the spec needs clarification. My intention is that it would apply to UA styles (for a given origin). However, I'm unsure on the exact implementation problems that could come from that. It's worth being aware Chromium actually doesn't support media feature queries in UA styles at all currently.) See https://github.com/WICG/web-preferences-api/issues/22
If the website changes, and don't provide the feature any more, how is the user going to get the behavior any more?
Resetting the preference overrides in the site settings of the browser, or clearing all site data depending on the UA's implementation (again I don't think the mechanism for clearing can be specced as it's UA dependent but I'm happy to put that the UA MUST provide a way to reset)
Another way of doing this is to add a custom media query.
Tab already answered this but just to emphasise custom media queries are not the answer. They don't support color-scheme changes, they don't work with third party content, they don't integrate with user preference client hint headers, they don't integrate with conditional resource loading (using the standard queries at least). They are a brilliant idea and I think a much needed addition to the platform but for the specifics of this they do not solve the problem.
I wanted to jump on the bandwagon and say that persistence is super scary
Again I'm keen to hear the reasoning for this? What "attacks" could persistence of this setting lead to. If it's purely about sites removing this functionality down the road and the user being stuck. While that's valid and I hope addressed by the fact UAs can clear this, it's worth noting that well sites can already completely ignore the user preferences so from the users perspective it's not all that different?
Are y'all saying "y'all committing to adding browser UI to allow per-site preference changes"? If you think it's a good idea, but you don't want to do it, i'd like to proceed
This is the key of my thoughts. If you're willing to commit to adding per site (or heck even a browser level toggle which some don't offer atm) settings then great. But until we all get that we're stuck with a rather poor status quo.
I really don't want this to become the password toggle button over again. To be completely frank put your money where you mouth is and implement this ability in the browser, or give us web developers the ability to do it ourselves.
Not to mention the fact that I highly doubt browser UI will stop sites doing their own for color scheme and being stuck in the same place as before.
I also don't think browser UI and this API need be mutually exclusive. It's perfectly plausible for the browser to offer UI that can sync with the site overrides. Like shown in Bramus' demo above.
- some of these preferences... i want concrete examples. some preferences have side effects apart from MQs. reduce-motion markes marquees not scroll in firefox. Disables smooth scrolling. The API should be explicit about whether that's effected
Completely valid I'll make an issue on the spec to address this, thanks for the specific on what needs looking at! Fwiw these should all be affected too. (again completely open to discussion) See https://github.com/WICG/web-preferences-api/issues/24
emilio: Browsers do expose per-sites settings for this, but not to the site, but they do for extensions. Firefox does have per-site setting override that extensions and users can use.
Again thanks for bringing this to my attention I wasn't aware such a feature existed. Again I'll raise an issue to discuss which should take precedence. I don't actually have answer off the top of my head to this one. My gut says the precedence should be OS < Browser < Site < Extension but needs discussion. See https://github.com/WICG/web-preferences-api/issues/25
As noted in https://github.com/WICG/web-preferences-api/issues/16#issuecomment-1710794442, changing the value from within script should have the proper safeguards. E.g. the UA could ask for confirmation the first time a site wants to change the value, similar to how sharing your geolocation works.
Yep the exact shape of the API has changed once I'll happily change it again to be async functions if we want permissions integrations etc :)
To update those following this issue, I've made two key changes that I hope will reduce concerns. The API now uses an async requestOverride function, to allow browsers more control of the process. "no-preference" values have also been removed from the initial version of the spec. So overrides can only opt-in rather than opt-out of accessibility features.
There's an initial implementation of the proposed API in chrome canary (requires a launch flag). Currently it doesn't persist the settings but I'm working on adding that.
I've also started working on a list of examples of sites with settings that could benefit from this API and specifically which preferences inside of it. https://github.com/WICG/web-preferences-api/issues/29
So overrides can only opt-in rather than opt-out of accessibility features.
I'm not sure I understand why this is helpful.
When switched on by the user/user-agent, the various prefers-*
MQ do not turn on accessibility features supplied by the user-agent. They inform the site that the user has a preference, and it's up to the site to do something about it. By default, nothing happens. If the site wants to ignore the user's preference, they already can, simply by doing nothing.
If the site has some reason to think that the user's preference (or lack thereof) is different from what it's getting from the ua, It can chose to do something else, or to do nothing. We cannot change that. It doesn't matter whether that comes from something reasonable like a settings dialog, or unreasonable like the author thinking that everyone surely agrees with their personal tastes: it's up to the author to supply the feature, so if they think they should not, there's nothing we can do about it.
So, I don't think we're protecting the user from anything by preventing the site from opting out.
Note: There are cases where the browser may combine a prefers-*
MQ with some behavior forcibly done by the browser, but that's a separate thing, and these overrides would have no influence on those. For example, User agents may consider setting prefers-reduced-data
based on the same user or system preference as they use to set the Save-Data
HTTP request header. But the Save-Data
HTTP request header is not caused by the prefers-reduced-data
MQ, and even if a site overrode that MQ, it would have no effect on the HTTP header.
The API proposal is to override the underlying preferences which would affect all those things not just the MQs. Hence the additional "protections" to try to alleviate concerns. See https://wicg.github.io/web-preferences-api/#sec-intro for details
The overall API shape looks good to me, moving this to a web preferences API, that can operate per-origin, seems to solve many more problems that have been mentioned above. It does feel like the right place to solve this problem.
Some observations.
It looks like this API is strictly for overrides so those wanting to offer a toggle would still need to do an initial check with matchMedia
to get the current value. Like below:
let currentValue = null;
// We don't know what the initial value is so lets get it.
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
currentValue = 'dark'
}
button.onClick(() => {
const newVal = (currentValue === 'dark') ? 'light' : 'dark';
navigator.preferences.colorScheme.requestOverride(newVal)
.then(() => {
// The preference override was successful.
})
.catch((error) => {
// The preference override request was rejected.
});
})
Something like this might be better
button.onClick(() => {
const newVal = (navigator.preferences.colorScheme.value === 'dark') ? 'light' : 'dark';
navigator.preferences.colorScheme.requestOverride(newVal)
.then(() => {
// The preference override was successful.
})
.catch((error) => {
// The preference override request was rejected.
});
})
It doesn't feel the most ergonomic. I wonder if the API provides the computed value we could skip the first step entirely. Looks like this is covered in: https://github.com/WICG/web-preferences-api/issues/7
Although I don't fully understand how the API shape https://github.com/WICG/web-preferences-api/issues/7#issuecomment-1721930664 solves this so it would be good to see an example.
For those who are implementing a toggle, I wonder if the fulfilled promise should return the value that is now set, ergonomically this would make it easy for website authors to pass that value to their currentSet
state.
I guess there's questions on how to deal with iframes (if we even need to). But that's being discussed here: https://github.com/WICG/web-preferences-api/issues/8
I guess now that this has moved into a potential browser API and into the WICG, I guess its no longer a CSS WG matter, unless any here feel against the direction this is in.
If people are happy with the control of this being in Web Preferences, then @tabatkins I would be interested in your thoughts on where https://github.com/WICG/web-preferences-api goes in terms of reviewal, does this go into the Web Apps working group?
Although I don't fully understand how the API shape https://github.com/WICG/web-preferences-api/issues/7#issuecomment-1721930664 solves this so it would be good to see an example.
The proposal and your idea for improvement is basically the same only difference is I've done values as an array because technically they could match multiple values at once. This isn't very ergonomic though so I would rather just do .value and accept that these media queries are single value at a time.
button.onclick = () => {
const newVal = (navigator.preferences.colorScheme.values[0] === 'dark') ? 'light' : 'dark';
navigator.preferences.colorScheme.requestOverride(newVal)
.then(() => {
// The preference override was successful.
})
.catch((error) => {
// The preference override request was rejected.
});
};
My proposal is to make each of the preference objects an event target too so you can listen for changes in them. That way you don't need to do use match media.
does this go into the Web Apps working group?
I think it's reasonable for this to graduate either to the CSSWG or the WebApps WG for standardization, with a slight preference for the CSSWG since it's so intimately connected to an existing CSS spec (Media Queries). We'd want to make sure any new preference MQs are reflected in this API, and it's a little easier to remember to do that when they live under the same WG.
Some updates on the web preferences API proposal.
It now has a .value property which addresses the ergonomics issue with needing matchMedia, the preference objects are now also event targets that fire a change event in various scenarios, this again is a big ergonomic win.
I feel like the spec and chromium prototype (which now should only be missing persistence) is in a state to progress this now.
Given the appetite for this to be in the csswg what's the best next steps?
Agenda+ to discuss adopting this into a CSSWG deliverable. (Maybe just folding into MQ5?)
It would be good if this could make it to the Agenda for the face to face, given I'll be there to discuss it. Do I need a different label for that?
Yup, tagged.
The CSS Working Group just discussed [mediaqueries-5] Script control of (prefers-*) queries
, and agreed to the following:
RESOLVED: Put this proposal into css-mediaqueries-5
RESOLVED: Add lwarlow as an editor for mediaqueries-5
In this Discourse thread, jimmyfrasche requests the ability for pages to override the (prefers-) MQs in script, so they can easily integrate a manual toggle in the page without having to do silly duplication of their styles in both an MQ and* a selector.
My initial response was that custom MQs would solve this, but they're actually somewhat non-trivial to do.
The trivial solution of "just make a script-based custom MQ that initially defers to (prefers-color-scheme), and can be overridden to true/false if you manually toggle" means that if script doesn't load, you have an unknown
(--dark-mode)
MQ in your page that is just always false, so you apply light-mode styles even if the user normally wants dark mode.Instead you must double-layer them:
And this only works for boolean MQs. If they're multi-state, it's trickier; you have to set up distinct custom MQs for each of the states and override all of them in script.
Would it instead make sense to allow authors to override at least the (prefers-*) queries with
CSS.customMedias
, supplying a CSS value that's valid for the MQ in question? That way you could instead write:(I don't see a particular reason to limit it to only the (prefers-) queries, but nor do I see a reason to allow* it for things like (width). I mildly lean towards allowing override of anything, just so we don't have to think about which ones are allowed and which aren't and keep that list up to date, but I could easily be convinced otherwise.)