40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | <<<<<<< HEAD
4 | Copyright (c) 2021 Gianmarco Simone
5 | =======
6 | Copyright (c) 2021 AlaricBaraou
7 | >>>>>>> master
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Build documentation and deploy to GitHub Pages
2 | on:
3 | push:
4 | branches: ['main']
5 | workflow_dispatch:
6 |
7 | # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency)
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build:
14 | uses: pmndrs/docs/.github/workflows/build.yml@v2
15 | with:
16 | mdx: 'docs'
17 | libname: 'A11y'
18 | home_redirect: '/introduction'
19 | icon: '♿️'
20 | logo: '/logo.jpg'
21 | github: 'https://github.com/pmndrs/react-three-a11y'
22 |
23 | deploy:
24 | needs: build
25 | runs-on: ubuntu-latest
26 |
27 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
28 | permissions:
29 | pages: write # to deploy to Pages
30 | id-token: write # to verify the deployment originates from an appropriate source
31 |
32 | # Deploy to the github-pages environment
33 | environment:
34 | name: github-pages
35 | url: ${{ steps.deployment.outputs.page_url }}
36 |
37 | steps:
38 | - id: deployment
39 | uses: actions/deploy-pages@v4
40 |
--------------------------------------------------------------------------------
/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as THREE from "three"
2 | import ReactDOM from 'react-dom/client';
3 | import React, { useRef, useState } from "react"
4 | import { Canvas, useFrame } from "@react-three/fiber"
5 | import { A11y, useA11y, A11yAnnouncer, A11yUserPreferences, useUserPreferences, A11ySection, A11yDebuger } from "../../"
6 |
7 | /* just to test tsx autocomplete etc */
8 | function Box(props: JSX.IntrinsicElements["mesh"]) {
9 | const mesh = useRef(null!)
10 | const [hovered, setHover] = useState(false)
11 | const [active, setActive] = useState(false)
12 | useFrame((state, delta) => (mesh.current.rotation.x += 0.01))
13 | return (
14 | {}} href="/">
15 | setActive(!active)}
20 | onPointerOver={event => setHover(true)}
21 | onPointerOut={event => setHover(false)}>
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | const rootElement = document.getElementById("root")
30 | const root = React.useMemo(() => ReactDOM.createRoot(rootElement), [rootElement])
31 |
32 | root.render(
33 |
39 | )
40 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | // output .d.ts declaration files for consumers
9 | "declaration": true,
10 | // output .js.map sourcemap files for consumers
11 | "sourceMap": true,
12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
13 | "rootDir": "./src",
14 | // stricter type-checking for stronger correctness. Recommended by TS
15 | "strict": true,
16 | "noFallthroughCasesInSwitch": true,
17 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | // use Node's module resolution algorithm, instead of the legacy TS one
21 | "moduleResolution": "node",
22 | // transpile JSX to React.createElement
23 | "jsx": "react",
24 | // interop between ESM and CJS modules. Recommended by TS
25 | "esModuleInterop": true,
26 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
27 | "skipLibCheck": true,
28 | // error out if import and file system have a casing mismatch. Recommended by TS
29 | "forceConsistentCasingInFileNames": true,
30 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
31 | "noEmit": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/docs/roles/link.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Link
3 | description: This page will tell you all you need to know about emulating an accessible link in your react-three-fiber app with @react-three-a11y
4 | nav: 6
5 | ---
6 |
7 | ## Role Link of the A11y component
8 |
9 | This role is fairly straightforward
10 | You should think of it as the equivalent of an html link "a" tag
11 | Since it's meant to emulate the behaviour of a regular html link. It should be used in combination with something that will trigger navigation on click.
12 |
13 | ```jsx
14 | {
18 | router.push(`/page`);
19 | }}
20 | >
21 |
22 |
23 | ```
24 |
25 | Using it like this makes it focusable to all kind of users. It will also show a pointer on mouse over.
26 |
27 | You should also use the useA11y() hook within the encapsulated components to adjust the rendering on hover and focus. Doing so greatly improve the accessibility of your page.
28 | Take a look at this code sample to see how to use it.
29 | You can also play with it in [this demo](https://n4rzi.csb.app)
30 |
31 | ```jsx
32 | function Some3DComponent() {
33 | const a11y = useA11y();
34 | return (
35 |
36 |
37 |
43 |
44 | );
45 | }
46 | ```
47 |
48 | > [!IMPORTANT]
49 | > Don't forget to provide the `href` attribute as he is required for screen readers to read it correctly!
50 | > It will have no effect on the navigation, it's just used as information
51 |
--------------------------------------------------------------------------------
/docs/roles/button.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Button
3 | description: This page will tell you all you need to know about emulating an accessible button in your react-three-fiber app with @react-three-a11y
4 | nav: 4
5 | ---
6 |
7 | ## Role Button of the A11y component
8 |
9 | This role is meant to emulate the behaviour of a button.
10 | It will display a cursor pointer when your cursor is over the linked 3D object.
11 | It will call a function on click but also on any kind of action that would trigger a focused button (Enter, Double-Tap, ...).
12 | It is also actionnable by user using a screen reader.
13 |
14 | ```jsx
15 | sendEmail()}
20 | ... >
21 |
22 |
23 | ```
24 |
25 | Using it like this makes it focusable to all kind of users.
26 |
27 | You should also use the useA11y() hook within the encapsulated components to adjust the rendering on hover and focus. Doing so greatly improve the accessibility of your page.
28 | Take a look at this code sample to see how to use it.
29 | You can also play with it in [this demo](https://n4rzi.csb.app)
30 |
31 | ```jsx
32 | function Some3DComponent() {
33 | const a11y = useA11y()
34 | return (
35 |
36 |
37 |
43 |
44 | )
45 | }
46 | ```
47 |
48 | You could also specify the optional prop `activationMsg`.
49 | The message withinh activationMsg will be announced by screenreader when the button is activated.
50 |
--------------------------------------------------------------------------------
/docs/advanced/how-it-works.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: How does it work?
3 | description:
4 | This is an advanced guide on the inner workings of @react-three/a11y, if you are just getting started, take a
5 | look at our introduction!
6 | nav: 7
7 | ---
8 |
9 | This library is pretty simple, if you're curious about how it works you can read the following information and look at the internals.
10 |
11 | @react-three/a11y purpose is to manage the accessibility part of what is inside your canvas by syncing semantic DOM absolutely positioned over what's currently visible in your page.
12 |
13 | Basically when you add the A11y component with a role, react-three-a11y will append the corresponding HTML tag to your document.
14 |
15 | - role="link" => a
16 | - role="button" => button
17 | - role="content" => p
18 | - role="togglebutton" => button ( + aria-pressed )
19 |
20 | The position is synced by a minimalist fork of the [Drei Html component](/drei/misc/html)
21 |
22 | Inside an A11y component, you can access the hover, focused and pressed state through the useA11y() hook.
23 | This hook returns the context of the A11y component.
24 |
25 | The A11yAnnouncer is used to communicate with screen readers through a div only visible to screen readers.
26 | It uses a [zustand](/zustand) store to update the div with each new message.
27 | The div is roughly like this.
28 |
29 | ```html
30 |
{message}
31 | ```
32 |
33 | The A11ySection component appends an HTML section in which a p element describe the content of the section.
34 | Wrapped around some A11y components, it will cause the HTML from those component to be inside the section.
35 |
36 | You would then have a generated DOM that could look something like this.
37 |
38 | ```html
39 |
40 |
description
41 |
42 |
43 |
44 |
45 | ```
46 |
47 | For the A11yUserPreferences component, it simply exposes through a context the state of prefers-color-scheme and prefers-reduced-motion media queries.
48 | It watches for change through
49 |
50 | ```javascript
51 | window.matchMedia('(prefers-reduced-motion: reduce)')
52 | window.matchMedia('(prefers-color-scheme: dark)')
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/roles/content.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Content
3 | description: This page will tell you all you need to know about making your content accessible in your react-three-fiber app with @react-three-a11y
4 | nav: 3
5 | ---
6 |
7 | ## Role Content of the A11y component
8 |
9 | This is the simplest role of all.
10 | You should think of it as the equivalent of an image alt attribute
11 | Whenever you have something in your canvas that is not simply decorative, you should use this role.
12 |
13 | Imagine you're displaying some text with the [three.js TextGeometry](https://threejs.org/docs/#api/en/geometries/TextGeometry)
14 |
15 | A user using a screen reader would not have access to the text.
16 |
17 | Let's say the text is 'Welcome to my website', you could simply do as below.
18 |
19 | ```jsx
20 |
21 |
22 |
23 | ```
24 |
25 | That's it !
26 |
27 | Now if you inspect the dom of your app, you will see that a `
` tag has been added with your text inside.
28 | That way, user with a screenreader will be able to read that text too.
29 |
30 | > [!NOTE]
31 | > For people using screen readers it will also sync some kind of focus indicator natively where your text is so people so screen readers users will know where they're currently in your page.
32 |
33 | This role can also be used to serve as a step for a user to navigate your site using Tab for instance.
34 | For that you would need to add the tabIndex prop and the focusCall prop like so.
35 |
36 | ```jsx
37 | someFunction()}
42 | >
43 |
44 |
45 | ```
46 |
47 | On focus, you could rotate the camera to show that second piece of text that would usually have required some scrolling to display.
48 | Use it as you please but keep in mind how it might impact the accessibility.
49 | For this example, screenreader don't trigger focus when swiping their screen so it would benefit people used to navigate through keyboard without hurting screenreader users.
50 |
51 | It's not meant to trigger anything on click or to be activable with the Keyboard. Therefore it won't show a pointer cursor on hover.
52 |
--------------------------------------------------------------------------------
/docs/advanced/gotchas.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Gotchas
3 | description: Things that could catch you off guard
4 | nav: 8
5 | ---
6 |
7 | ### Position of the accessible dom causing wrong hover detection
8 |
9 | In the demo, you may have noticed in App.js that the component ToggleButton is wrapped in an A11y component that use the prop `a11yElStyle` like so
10 |
11 | ```jsx
12 | (state.dark = !snap.dark)}
16 | activationMsg="Lower light disabled"
17 | deactivationMsg="Lower light enabled"
18 | a11yElStyle={{ marginLeft: '-40px' }}
19 | >
20 |
21 |
22 | ```
23 |
24 | Why is that ?
25 |
26 | In order to make this "donut" accessible as a button react-three-a11y will keep an html button over it.
27 |
28 | If we inspect the DOM, you should see something roughly like this for the above example.
29 |
30 | ```html
31 |
32 | ```
33 |
34 | By default this button is positioned in the center of your 3D object which would cause it to work like this.
35 |
36 | 1- Mouse is not over
37 |
38 |
39 |
40 |
41 |
42 | 2- Mouse is over the donut, color change is triggered and the cursor pointer is displayed
43 |
44 |
45 |
46 |
47 |
48 | 3- Mouse is not over the donut, but the color change is still triggered as is the cursor pointer
49 |
50 |
51 |
52 |
53 |
54 | If we display the button we can see that it's caused by the button being positioned in the middle of the donut
55 |
56 |
57 |
58 |
59 |
60 | 4- If we add `a11yElStyle={{ marginLeft: '-40px' }}` to the A11y component, the button is moved to the left and not in the center of the donut anymore
61 |
62 |
63 |
64 |
65 |
66 | 5- And as we can see, it fixes our issue. The cursor is default and no color change while the cursor is in the hole of the donut.
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/docs/roles/togglebutton.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: ToggleButton
3 | description: This page will tell you all you need to know about emulating an accessible togglable button in your react-three-fiber app with @react-three-a11y
4 | nav: 5
5 | ---
6 |
7 | ## Role ToggleButton of the A11y component
8 |
9 | This is mostly the same as the [button role](/a11y/roles/button).
10 | The difference is that this button will have the aria-pressed attribute and that you'll be able to use the following deactivationMsg property in addition to the usual description and activationMsg properties.
11 |
12 | Since this role is meant to emulate the behaviour of togglable button.
13 | It will display a cursor pointer when your cursor is over the linked 3D object.
14 | It will call a function on click but also on any kind of action that would trigger a focused button (Enter, Double-Tap, ...).
15 | It is also actionnable by user using a screen reader.
16 |
17 | ```jsx
18 |
24 |
25 |
26 | ```
27 |
28 | Using it like this makes it focusable to all kind of users.
29 |
30 | > [!NOTE]
31 | > You might have noticed the startPressed prop. Depending on your need, you might want to have your button starting in a pressed state. This is what this prop is for.
32 |
33 | You should also use the useA11y() hook within the encapsulated components to adjust the rendering on hover and focus and pressed state. Doing so greatly improve the accessibility of your page.
34 | Take a look at this code sample to see how to use it.
35 | You can also play with it in [this demo](https://n4rzi.csb.app)
36 |
37 | ```jsx
38 | function Some3DComponent() {
39 | const a11y = useA11y(); // access pressed, hover and focus
40 | return (
41 |
42 |
43 |
49 |
50 | );
51 | }
52 | ```
53 |
54 | You could also specify the optional prop `activationMsg` and `deactivationMsg`.
55 | Respective message will be announced by screenreader when the button is activated / deactivated.
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-three/a11y",
3 | "version": "3.0.1",
4 | "description": "👩🦯 Provide accessibility support to R3F such as focus indication, keyboard tab index, and screen reader support",
5 | "keywords": [
6 | "a11y",
7 | "accessibility",
8 | "three",
9 | "react",
10 | "react-three-fiber"
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/pmndrs/react-three-a11y.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/pmndrs/react-three-a11y/issues"
18 | },
19 | "author": "Alaric Baraou",
20 | "license": "MIT",
21 | "main": "dist/index.js",
22 | "module": "dist/a11y.esm.js",
23 | "typings": "dist/index.d.ts",
24 | "files": [
25 | "dist",
26 | "src"
27 | ],
28 | "engines": {
29 | "node": ">=10"
30 | },
31 | "scripts": {
32 | "start": "tsdx watch",
33 | "build": "tsdx build",
34 | "test": "tsdx test --passWithNoTests",
35 | "lint": "tsdx lint --fix",
36 | "prepare": "tsdx build",
37 | "size": "size-limit",
38 | "analyze": "size-limit --why",
39 | "storybook": "start-storybook -p 6006",
40 | "build-storybook": "build-storybook"
41 | },
42 | "peerDependencies": {
43 | "@react-three/fiber": ">=8.0",
44 | "react": ">=18.0",
45 | "react-dom": ">=18.0",
46 | "three": ">=0.137"
47 | },
48 | "dependencies": {
49 | "utility-types": "^3.10.0",
50 | "zustand": "^3.5.13"
51 | },
52 | "husky": {
53 | "hooks": {
54 | "pre-commit": "tsdx lint"
55 | }
56 | },
57 | "prettier": {
58 | "printWidth": 80,
59 | "semi": true,
60 | "singleQuote": true,
61 | "trailingComma": "es5"
62 | },
63 | "size-limit": [
64 | {
65 | "path": "dist/a11y.cjs.production.min.js",
66 | "limit": "10 KB"
67 | },
68 | {
69 | "path": "dist/a11y.esm.js",
70 | "limit": "10 KB"
71 | }
72 | ],
73 | "devDependencies": {
74 | "@babel/core": "^7.14.3",
75 | "@react-three/fiber": "^8.0.8",
76 | "@size-limit/preset-small-lib": "^11.0.2",
77 | "@storybook/addon-essentials": "^7.0.12",
78 | "@storybook/addon-info": "^5.3.21",
79 | "@storybook/addon-links": "^7.0.12",
80 | "@storybook/addons": "^7.0.12",
81 | "@storybook/react": "^7.0.12",
82 | "@types/jest": "^26.0.10",
83 | "@types/lodash-es": "^4.17.3",
84 | "@types/react": "^17.0.5",
85 | "@types/react-dom": "^18.0.4",
86 | "@types/three": "^0.149.0",
87 | "babel-loader": "^8.2.2",
88 | "husky": "^6.0.0",
89 | "react": "^18.0.0",
90 | "react-dom": "^18.0.0",
91 | "react-is": "^18.0.0",
92 | "size-limit": "^11.0.2",
93 | "three": "^0.149.0",
94 | "tsdx": "^0.14.1",
95 | "tslib": "^2.1.0",
96 | "typescript": "^4.7.4"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/A11ySection.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | useEffect,
4 | useRef,
5 | MutableRefObject,
6 | createRef,
7 | } from 'react';
8 | import { useThree } from '@react-three/fiber';
9 | import { stylesHiddenButScreenreadable } from './A11yConsts';
10 |
11 | interface Props {
12 | children: React.ReactNode;
13 | label: string;
14 | description: string;
15 | }
16 |
17 | const A11ySectionContext = React.createContext<
18 | MutableRefObject
19 | >(createRef());
20 |
21 | A11ySectionContext.displayName = 'A11ySectionContext';
22 |
23 | const useA11ySectionContext = () => {
24 | return useContext(A11ySectionContext);
25 | };
26 |
27 | export { useA11ySectionContext };
28 |
29 | export const A11ySection: React.FC = ({
30 | children,
31 | label,
32 | description,
33 | }) => {
34 | const ref = useRef(null);
35 | const refpDesc = useRef(null);
36 | const gl = useThree((state) => state.gl);
37 | const [el] = React.useState(() => document.createElement('section'));
38 | const target = gl.domElement.parentNode;
39 |
40 | useEffect(() => {
41 | // eslint-disable-next-line react-hooks/exhaustive-deps
42 | if (label) {
43 | el.setAttribute('aria-label', label);
44 | }
45 | el.setAttribute('r3f-a11y', 'true');
46 | el.setAttribute(
47 | 'style',
48 | ((styles) => {
49 | return Object.keys(styles).reduce(
50 | (acc, key) =>
51 | acc +
52 | key
53 | .split(/(?=[A-Z])/)
54 | .join('-')
55 | .toLowerCase() +
56 | ':' +
57 | (styles as any)[key] +
58 | ';',
59 | ''
60 | );
61 | })(stylesHiddenButScreenreadable)
62 | );
63 | if (description) {
64 | if (refpDesc.current === null) {
65 | const pDesc = document.createElement('p');
66 | pDesc.innerHTML = description;
67 | pDesc.style.cssText =
68 | 'border: 0!important;clip: rect(1px,1px,1px,1px)!important;-webkit-clip-path: inset(50%)!important;clip-path: inset(50%)!important;height: 1px!important;margin: -1px!important;overflow: hidden!important;padding: 0!important;position: absolute!important;width: 1px!important;white-space: nowrap!important;';
69 | el.prepend(pDesc);
70 | refpDesc.current = pDesc;
71 | } else {
72 | refpDesc.current.innerHTML = description;
73 | }
74 | }
75 | return () => {
76 | if (target) target.removeChild(el);
77 | };
78 | }, [description, label]);
79 |
80 | if (ref.current === null) {
81 | if (target) {
82 | target.appendChild(el);
83 | }
84 | ref.current = el;
85 | }
86 |
87 | return (
88 | <>
89 |
90 | {children}
91 |
92 | >
93 | );
94 | };
95 |
--------------------------------------------------------------------------------
/docs/access-user-preferences.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: User Preferences
3 | description: Learn how to access user preferences and how to adjust your app using react-three-fiber with @react-three-a11y
4 | nav: 2
5 | ---
6 |
7 | When it comes to accessibility, some users might need to have an interface as still as possible or with a preferred color scheme.
8 |
9 | These are CSS media features and this library expose two of them
10 |
11 | 1. [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
12 | 1. [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
13 |
14 | While you don't necessarily require this library to access them, we provide an easy way to use them and listen to their changes.
15 |
16 | ## Setup
17 |
18 | For that wrap your components inside the A11yUserPreferences component
19 |
20 | ```jsx
21 |
22 |
23 |
24 | [...]
25 |
26 | ```
27 |
28 | Then, you can access the preferences in each children component where you might need them.
29 |
30 | ## Reduce motions / animations for the users that request it
31 |
32 | Some user on your website might need all animation turned off or limited to what's strictly necessary.
33 | For those users, if your app has an animation going somewhere, consider cancelling it for those who request it.
34 | You can do it like so.
35 |
36 | ```jsx
37 | const My3dObject = () => {
38 | // this const will give you access to the user preferences
39 | const { a11yPrefersState } = useUserPreferences()
40 | const mesh = useRef()
41 |
42 | // Rotate mesh every frame
43 | useFrame(() => {
44 | //unless the user prefers reduced motion
45 | if (!a11yPrefersState.prefersReducedMotion) {
46 | mesh.current.rotation.x = mesh.current.rotation.y += 0.01
47 | }
48 | })
49 |
50 | return (
51 |
52 |
53 |
54 |
55 | )
56 | }
57 | ```
58 |
59 | ## Adapt colour scheme depending on the user preference
60 |
61 | Some user on your website might need a darker / lighter theme. You can adapt your components according to it like so.
62 |
63 | ```jsx
64 | const My3dObject = () => {
65 | // this const will give you access to the user preferences
66 | const { a11yPrefersState } = useUserPreferences()
67 | const mesh = useRef()
68 |
69 | return (
70 |
71 |
72 |
73 |
74 | )
75 | }
76 | ```
77 |
78 | ## Use the context outside and inside the r3f canvas
79 |
80 | At the moment React context [can not be readily used between two renderers](https://github.com/pmndrs/react-three-fiber/issues/43), this is due to a problem within React.
81 | If react-dom use the A11yUserPreferences provider, you will not be able to consume it within `