79715892

Date: 2025-07-26 18:16:23
Score: 1
Natty:
Report link

Explanation of the problem

Reading the question and comments, I believe there's a (common) little misunderstanding here on how @media () { works.

Using this example:

@media (prefers-contrast: more) { ...

I get the sense you're thinking the @at-rule above is like:

if (getPrefersContrast() == "more") { ...

When in reality it's more of a:

if (userPrefersMoreContrast() == true) {...

What I mean by this is, CSS asks the browser a question, and the browser only returns true or false. CSS has no idea about the existance of other potential values; 'less', 'custom, etc. In CSS's eyes @media (prefers-contrast: banana) is a perfectly valid media query, and this case the browser will return "false" just like it would if the user simply didn't "prefer 'more' contrast".

JavaScript's window.matchMedia(), for better or for worse, was designed to perfectly replicate what CSS does. So, just like CSS, JS has no idea what potential prefers-contrast values could exist, all it has the power to do is ask whether one specific one does exist, and get a yes or no response.

Unlike CSS & JS, we as the developer, have access to the "The Documentation" and therefore we possess enough clairvoyance to know the exact values that could exist.

Solutions

With that being said, to answer the original question, no there's no alternate method. However, I did think of two methods which would reduce repeatability.

1. Since CSS & JS "can't", we manually provide an array of all the respective values and loop over each, and apply the change event

You already hinted at this solution and stated that it's not future-proof "what if a 'custom' value is added?", but it's even worst too since it's not cross-browser-proof either, what if one browsers adds an experimental '-webkit-high-contrast', or what if one browser simply lags behind and doesn't yet support the W3C standards.

While the list we provided is guaranteed to be not entirely correct in some cases, this idea/pattern is actually common practise and used all the time in web development. For example, since there's no transition-state-changed event, it's very common to see code like: ['transitionrun', 'transitionstart', 'transitioncancel', 'transitionend'].forEach(eventName => elem.addEventListener(eventName, someHandler));.

Similarly:

// ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => ...
or
// ['pointerdown', 'mousedown', 'touchstart']
or
// ['requestAnimationFrame','webkitRequestAnimationFrame','mozRequestAnimationFrame','oRequestAnimationFrame',"msRequestAnimationFrame']

So, it's clear to see, while there are cons. It's a compromise a lot of developers are willing to make (or rather 'concede' would be a better word to use). Plus, while it may seem "brute force" as you say... that's kinda the whole point of utility functions, to convert the brute force, repetitive tasks, into a single one-use method call.

Solution #1 would look like:

function getContrastPreference() {
  const contrastOptions = ['more', 'less', 'custom'];

  for (const option of contrastOptions) {
    const mediaQuery = `(prefers-contrast: ${option})`;
    if (window.matchMedia(mediaQuery).matches) {
      return option;
    }
  }

  // If none, return the default
  return 'no-preference';
}

2. The other option is to let CSS do what it does best, use the @at-rules as usually, and store the result

:root {
  /* Defaults */
  --prefers-contrast: 'no-preference';
  --prefers-color-scheme: 'light';
  ...
}

/* Update when the media query matches */
@media (prefers-contrast: more)   { :root { --prefers-contrast: 'more'; } }
@media (prefers-contrast: less)   { :root { --prefers-contrast: 'less'; } }
@media (prefers-contrast: custom) { :root { --prefers-contrast: 'custom'; } }

@media (prefers-color-scheme: dark) { :root { --prefers-color-scheme: 'dark'; } }
function getMediaFeatureFromCSS(propertyName) {
  const value = getComputedStyle(document.documentElement).getPropertyValue(propertyName);

  // Clean up result
  return value.trim().replace(/['"]/g, '');
}

console.log(getMediaFeatureFromCSS('--prefers-contrast')); // "dark"
// getMediaFeatureFromCSS('--prefers-color-scheme');

3. As a bonus, when I read your comments, it sounded like you were wishing for something like this to exist:

window.media.addEventListener('prefers-contrast', (event) => {
  // The event payload would contain the new value
  console.log('The new contrast preference is: ' + event.value); // e.g; 'more'
});

The thing is, the beauty about the current system is that you can create this yourself, by expanding upon Solution #1. I'm not going to do this for you, too much effort, but it's not even just "possible", I'm sure someone has probably done it already.

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • User mentioned (1): @at-rule
  • User mentioned (0): @at-rules
  • Looks like a comment (1):
Posted by: Tigerrrrr