sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
80.19k stars 4.27k forks source link

Built-in support for respecting the `prefers-reduced-motion` flag with animations/transitions #5346

Open rdmurphy opened 4 years ago

rdmurphy commented 4 years ago

Is your feature request related to a problem? Please describe. I was thinking about new ways to honor a user's request to have a less frustrating experience with animations and eventually wondered if this flag is something Svelte itself could (whether as default on or default off) "respect."

Describe the solution you'd like If Svelte respected prefers-reduced-motion out of the box, it could opt-out of any transitions or animations automatically without extra effort on the developer's part. Alternatively, this could be a flag to the various animation features that signal whether they should skip an animation instruction if prefers-reduced-motion is on.

Describe alternatives you've considered I've done this in the past manually (in three.js/WebGL land) so I think it's certainly doable by the developer if left to their own devices, and can be done today in Svelte without formal support from the library.

How important is this feature to you? I've always appreciated how Svelte helps developers do the right thing, and tries to lessen the pain of the "right thing" if you were left to coding it yourself. If a user is making the request to not have gratuitous or potentially harmful animations, I believe we should do whatever we can to respect that. In my opinion, this feels like a prime candidate for Svelte to encourage and enable the humane choice of respecting the user's request.

pushkine commented 4 years ago

All animations/transitions in svelte are done through pure css, simply add the following to your global style sheet

@media (prefers-reduced-motion: reduce) {
  * {
    animation-delay: 0ms !important;
    animation-duration: 1ms !important;
  }
}
blairn commented 4 years ago

All animations/transitions in svelte are not done through pure css.

Tweening is VERY much not done though css. The entire motion package isn't CSS

pushkine commented 4 years ago

svelte/motion is an extension of svelte/store, if you know for sure that setting your values instantly won't break your uses of spring and tweened, the apis are so similar you'll most likely be fine just re-exporting them as writables

import { spring as svelte_spring, tweened as svelte_tweened } from "svelte/motion";
import { writable } from "svelte/store";

const reduce_motion = matchMedia("(prefers-reduced-motion: reduce)").matches;

export const spring = reduce_motion ? writable : svelte_spring;
export const tweened = reduce_motion ? writable : svelte_tweened;

That's honestly as far as you'll get in terms of support for that feature given the constrains set by the v3 design

benaltair commented 3 years ago

I'm glad this was here - I was drafting a separate issue.

Svelte Transitions don't respect prefers-reduced-motion preferences - A11y

Describe the bug Svelte transitions don't seem to respect prefers-reduced-motion settings which could be an accessibility concern for users that struggle with movement on the screen.

To Reproduce Navigate to the tutorial for animations (or create an example locally in Svelte). https://svelte.dev/tutorial/adding-parameters-to-transitions

Open up the browser dev tools and set prefers-reduced-motion emulation to reduce. https://developer.chrome.com/blog/new-in-devtools-79/#userpreferences In Chrome for example, this is done in the Rendering panel which needs to be enabled. image

Expected behavior This is a bit difficult. I can't imagine a way that would solve this with no developer input. Perhaps reducing the max duration? An a11y expert in this area might be consulted to understand what about motion triggers people (for nausea, disorientation etc). While reducing max duration might help, it would be best to actually be able to choose a different animation for users who have their preferences set as such. For example, fly would be inappropriate in many cases for users who prefer reduced motion, whereas fade would be much more comfortable.

I think it's important to note that animation is still a useful tool for understanding state change and removing it entirely would be taking away an intuitive user signal to a portion of the population.

I wonder if we could just fall back to fade with a short duration time for all transitions when this setting is on? What would be the implications of this?

Ideally though, developers would be able to add transition directives for these cases. Something like reduceTransition, reduceIn, and reduceOut if they wish to override the default a11y behaviour.

benaltair commented 3 years ago

This blog post was suggested on Discord by @geoffrich. It has strategies (some similar to above) and also discusses the reasoning behind doing this. I know @Rich-Harris has spoken about the importance of supporting developers to do that right thing when it comes to a11y and I wonder if this is within that scope?

Blog Post

https://geoffrich.net/posts/accessible-svelte-transitions/

geoffrich commented 3 years ago

Fresh off a Svelte Summit talk about this very topic, I have some thoughts! I agree that Svelte has a responsibility to do something here, since it includes animation as part of its core library. However, I don't think it should be prescriptive as to how it respects a request for reduced motion. Svelte should not:

There's too much nuance in designing animation to make the right choice for every app. How to handle reduced motion is a design question, and there won't be a hard rule the framework can implement that will work for everyone.

Instead, I think the framework should document the need to respect reduced motion in the relevant tutorial sections and docs. While prefers-reduced-motion has been around for a couple years, many developers have no idea it exists. Presenting the need to think about these things would go a long way towards making the web more accessible.

In addition, I think we should include a built-in way to detect if reduced motion is enabled or not. Animation libraries like framer motion already do this with the useReducedMotion hook. While the actual code to do this in Svelte via a custom store is minimal, making it part of the core library would reduce friction and make it easy for developers to do the right thing.

Ideally, devs would be able to do something like this...

<script>
    import { reducedMotion } from 'svelte/motion';
    import { fly } from 'svelte/transition';

    let show = false;
</script>

<label><input type="checkbox" bind:checked={show}> Show/hide</label>
{#if show}
<h1 transition:fly={{y: $reducedMotion ? 0 : 400}}>Hello world!</h1>
{/if}

Let me know what you think of this approach. I think it's another way Svelte can show its commitment to a11y. I'd be happy to put together a PR if I get positive signals on this.

dominikg commented 3 years ago

+1 to not switching fly to fade (or something else) by default if we detect reducedMotion. But i wonder if we can do more than providing tools people need to implement themselves.

Wild ideas, tear them apart if not feasible

1) add compile time warnings to motionful transitions without a fallback

So people are warned when they neglect reducedMotion (similar to some a11y warnings we already have) Warning: in Foo.svelte you are using a motionful transition without respecting prefers-reduced-motion, read some-link why this is important.

2) add a generic way to select a different transition on reduced-motion

So people have tools to add their own use-case aware fallback when needed

<script>
    import { fly, fade, none } from 'svelte/transition';

    let show = false;
</script>

<label><input type="checkbox" bind:checked={show}> Show/hide</label>
{#if show}
<h1 transition:fly={{y:  400}} transitionReduced:fade={{opacity:1}} >Hello world!</h1>
{/if}
{#if show}
<h1 transition:fly={{y: 400}} transitionReduced:none >Hello world!</h1>
{/if}

3) implement default transitions that fall back to fade or none on a separate "reducedMotionAware" namespace

As a turn-key solution that fits most use-cases to make it as simple as possible to adopt.

<script>
    import { flyOrFade as fly } from 'svelte/transition/reducedMotionAware';
        // or import { flyOrNone as fly } from 'svelte/transition/reducedMotionAware';

    let show = false;
</script>

<label><input type="checkbox" bind:checked={show}> Show/hide</label>
{#if show}
<h1 transition:fly={{y:  400}}>Hello world!</h1>
{/if}

This would allow people to just swap out an import and be done, no other code changes needed. Less work => more adoption

clozach commented 2 years ago

Fwiw, I like the 2nd approach the best.

Warnings (# 1) become easier to ignore the more there are of them, and there are many circumstances which require no a11y attention (e.g., exploratory coding). Exposure in such circumstances is likely to desensitize us to such warnings, rather than prompting action. (Not saying that all warnings are bad…just that they're rarely an ideal solution.

The alternative motion library approach (# 3) seems too heavy-handed to meet real-world needs. As @geoffrich points out above, and in his excellent Svelte Summit talk, simply turning off animations when prefers-reduced-motion lacks the nuance needed in real-world situations. To quote Geoff, "it's prefers reduced motion, not prefers no motion." Among other, smaller concerns, this option also seems like a maintenance hassle as it would mean ensuring that svelte/transition and svelte/transition/reduceMotionAware remain swappable in perpetuity.

transitionReduced (# 2) appeals on several levels. It's almost self-explanatory, so a brief paragraph in the tutorial and another in the docs should be all that's needed, documentation-wise. In using the same syntax and behavior as transition, it retains the full power of Svelte transitions, while squirreling away the boilerplate needed to detect- and react to the user's motion preferences. It should even play well with auto-formatting:

  <h1
    transition:fly={{ y: 400 }}
    transitionReduced:fade={{ opacity: 1 }}
    style={$dark ? "white" : "black"}
  >

I also like Geoff's $reducedMotion idea, but it feels less Svelty than transitionReduced. Of course, these two ideas aren't mutually exclusive, and something like $reducedMotion would be needed under the hood to implement transitionReduced, so this might just be a question of "to expose or not to expose" when it comes to the lower-level feature.

geoffrich commented 2 years ago

Thanks for your thoughts, @dominikg and @clozach.

Adding warnings (option 1) is a good thought, though I'm a little hesitant since I don't think we could perfectly determine whether or not to show a warning. For example, using fly without a reduced motion fallback isn't always an issue -- if the element only flies a few pixels, it could be okay. And they could be accounting for reduced motion in a separate file entirely, e.g. in their base CSS. So I think a warning would be tricky to correctly implement -- there's too much subjectivity in what counts as a violation.

Add a /reducedMotionAware namespace is also interesting, though that still puts Svelte in the position in determining what the fallback should be. I do appreciate the desire to make this simple for users, but I think it makes too many decisions (what the fallback should be) that should be left to individual apps.

I find the transitionReduced directive most compelling, since it makes it easy to provide a fallback without having to import a separate store. Though what would replace in: and out: -- inReduced and outReduced? Maybe instead of a separate directive, we could make it a modifier like |local, so it would become transition|reduced, in|reduced, and out|reduced. We'd also have to define which transition should apply when, since we want |reduced to take precedence over a non-modified transition directive. I haven't entirely thought through this, but I think it should be possible.

e.g. consider this:

transition:fly
in|reduced:fade

Even if we did have a modifier for it, I still think there's value in exposing the underlying store to give the user even more control. For example, you could then tweak individual parameters on the transition based on the store instead of adding transition|reduced. This would also allow people to take reduced motion into account when using the tweened or spring stores for motion.

In summary, I'm now proposing the following:

  1. Expose reducedMotion store as part of svelte/motion
  2. Add new transition modifier reduced to make it easy to define a transition that should take precedence when reduced motion is enabled.

I expect more discussion to be needed around adding a new modifier. I think these could land in separate PRs -- having the store would deliver value if the modifier behavior is still being worked out.

benaltair commented 2 years ago

Ideally though, developers would be able to add transition directives for these cases. Something like reduceTransition, reduceIn, and reduceOut if they wish to override the default a11y behaviour.

<h1 transition:fly={{y:  400}} transitionReduced:fade={{opacity:1}} >Hello world!</h1>

I think having additional directives would have the added benefit of allowing for helpful tooling in the IDE. There could be some form of hint suggesting that a user add these directives in certain instances.

Not sure if it muddies things, but this tip could show only when the transition met certain thresholds. This introduces other issues though, and likely a universal solution (not case/criteria-based) would be more elegant. Regardless, having the option to implement alternate transitions in-line like this is developer-friendly.

justingolden21 commented 1 year ago

A simple implementation idea:

transition:fly={{y: 400, animteWithReducedMotion: false }}

with something like animteWithReducedMotion that's perhaps better named and defaults to true in order to not break existing code.

braebo commented 1 month ago

Honorable mention: https://github.com/ghostdevv/svelte-reduced-motion