├── .codesandbox └── ci.json ├── .github └── workflows │ ├── docs.yml │ ├── main.yml │ └── size.yml ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── access-user-preferences.mdx ├── advanced │ ├── beforehoverdefault.png │ ├── gotchas.mdx │ ├── hoverdefault.png │ ├── hoverdefaultbecause.png │ ├── hoverdefaultout.png │ ├── hoverfix.png │ ├── hoverfixbecause.png │ └── how-it-works.mdx ├── introduction.mdx ├── logo.jpg ├── react-three-a11y.jpg └── roles │ ├── button.mdx │ ├── content.mdx │ ├── link.mdx │ └── togglebutton.mdx ├── example ├── .prettierrc ├── index.html ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.js │ ├── index.js │ ├── index.tsx │ └── styles.css └── yarn.lock ├── package.json ├── src ├── A11y.tsx ├── A11yAnnouncer.tsx ├── A11yConsts.tsx ├── A11yDebuger.tsx ├── A11ySection.tsx ├── A11yUserPreferences.tsx ├── Html.tsx ├── announceStore.tsx └── index.tsx ├── stories └── Thing.stories.tsx ├── test └── blah.test.tsx ├── tsconfig.json └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["vecdv"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | .example/.parcel-cache 6 | dist 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'], 3 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 4 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration 5 | typescript: { 6 | check: true, // type-check stories during Storybook build 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters 2 | export const parameters = { 3 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 4 | actions: { argTypesRegex: '^on.*' }, 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "editor.formatOnSave": true, 7 | "[typescriptreact]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

@react-three/a11y

2 | 3 | [![Version](https://img.shields.io/npm/v/@react-three/a11y?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@react-three/a11y) 4 | [![Downloads](https://img.shields.io/npm/dt/@react-three/a11y.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@react-three/a11y) 5 | [![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/ZZjjNvJ) 6 | 7 | ![Imgur](https://i.imgur.com/sSAD7m7.png) 8 | 9 | @react-three/a11y brings accessibility to webGL with easy-to-use react-three-fiber components: 10 | 11 | - Focus and focus indication 12 | - Tab index and keyboard navigation 13 | - Screen reader support and alt-text 14 | - Roles and cursor shapes 15 | - Descriptive links 16 | 17 | You can try a [live demo here](https://n4rzi.csb.app). 18 | 19 | ```bash 20 | npm install @react-three/a11y 21 | ``` 22 | 23 | ## Quick overview to get started 24 | 25 | note: The full documentation can be found on the [pmndrs website](https://docs.pmnd.rs/a11y/introduction). 26 | 27 | ### The A11yAnnouncer component 28 | 29 | First, place the A11yAnnouncer component next to the R3F Canvas component. this componant is critical since it manage some screen-reader features. 30 | 31 | ```jsx 32 | import { Canvas } from '@react-three/fiber'; 33 | import { A11yAnnouncer } from '@react-three/a11y'; 34 | 35 | function App() { 36 | return ( 37 | <> 38 | 39 | 40 | 41 | ); 42 | } 43 | ``` 44 | 45 | ### Then wrap components you want to make accessible with the A11y component 46 | 47 | To add accessibility features to your scene you'll have to wrap components you want to make focusable with the `A11y` component: 48 | 49 | ```jsx 50 | import { A11y } from '@react-three/a11y' 51 | [...] 52 | 53 | 54 | 55 | ``` 56 | 57 | `MyComponent` can now receive focus. More accurately, the emulated "focus" will be handled at the `A11y` components which acts as a provider 58 | for children to access its state. But even if objects are focusable, nothing will be displayed or shown by default. 59 | 60 | ## Call function on focus 61 | 62 | The `focusCall` prop of `A11y` will be called each time this component receives focus (usually through tab navigation). 63 | 64 | ```jsx 65 | console.log("in focus")} ... /> 66 | ``` 67 | 68 | ## Call function on click / keyboard Click 69 | 70 | The `actionCall` prop of `A11y` will be called each time this component gets clicked, focused, keyboard activated etc. 71 | 72 | ```jsx 73 | console.log("clicked")} ... /> 74 | ``` 75 | 76 | ## Provide a description of the currently focused / hovered element 77 | 78 | When using the `description` prop in combination with the `role` prop, the `A11y` component will provide a description to the screen reader users on focus/hover. 79 | Optionally, you can also show the description to the user on hover by setting `showAltText={true}`. 80 | 81 | ```jsx 82 | // Reads "A rotating red square" to screen readers on focus / hover while also showing it on mouseover 83 | 84 | // Reads "Button, open menu + (description on how to activate depending on the screen reader)" to screen readers on focus / hover 85 | {someFunction()}} ... /> 86 | ``` 87 | 88 | ## The four roles of the A11y component 89 | 90 | Like in HTML, you can focus different kind of elements and expect different things depending on what you're focusing. 91 | 92 | #### Content 93 | 94 | ```jsx 95 | 96 | ``` 97 | 98 | Uses the `default` cursor. This role is meant to provide information to screen readers or to serve as a step for a user to navigate your site using Tab for instance. 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. 99 | 100 | [Read more about role content](/a11y/roles/content) 101 | 102 | #### Button 103 | 104 | ```jsx 105 | 109 | ``` 110 | 111 | Uses the `pointer` cursor. Special attributes: `activationMsg`. 112 | 113 | This role is meant to emulate the behaviour of a button or a togglable button. It will display a cursor pointer when your cursor is over the linked 3D object. It will call a function on click but also on any kind of action that would trigger a focused button (Enter, Double-Tap, ...). It is also actionnable by user using a screen reader. 114 | 115 | [Read more about role button](https://docs.pmnd.rs/a11y/roles/button) 116 | 117 | #### ToggleButton 118 | 119 | By using the role togglebutton, you'll emulate a button with two state that will have the `aria-pressed` attribute. 120 | You'll then be able to use the deactivationMsg property in addition to the usual description and activationMsg properties. 121 | 122 | ```jsx 123 | 128 | ``` 129 | 130 | Special attributes: `deactivationMsg` 131 | 132 | [Read more about role ToggleButton](https://docs.pmnd.rs/a11y/roles/togglebutton) 133 | 134 | #### Link 135 | 136 | ```jsx 137 | 138 | ``` 139 | 140 | Uses the `pointer` cursor. Special attributes: `href`. 141 | 142 | This role is meant to emulate the behaviour of a regular html link. It should be used in combination with something that will trigger navigation on click. 143 | 144 | > [!NOTE] 145 | > Don't forget to provide the href attribute as he is required for screen readers to read it correctly ! - It will have no effect on the navigation, it's just used as information 146 | 147 | [Read more about role link](https://docs.pmnd.rs/a11y/roles/link) 148 | 149 | ## Screen Reader Support 150 | 151 | In order to provide informations to screen reader users and use this package at its full potential, fill the `description` prop of all your `A11y` components and use the appropriate `role` prop on each of them. 152 | 153 | ### Use of section 154 | 155 | For screen readears, it might be useful to provide additionnal information on how to use some unconventional UI. 156 | You can do it by wrapping the concerned part of your code relative to this UI in the A11ySection like so. 157 | 158 | ```jsx 159 | 163 | [...] 164 | 165 | ``` 166 | 167 | ## Access user preferences 168 | 169 | The A11yUserPreferences component is available in order to access user preferences such as 170 | 171 | - prefers-reduced-motion 172 | - prefers-color-scheme 173 | 174 | Take a look at [the A11yUserPreferences page](https://docs.pmnd.rs/a11y/access-user-preferences) or the [demo](https://n4rzi.csb.app) to see it in action and how to use it. The demo will adapt to your system preferences. 175 | 176 | ## Additionals Features 177 | 178 | Use a custom tabindex with for your A11y components by providing a number to the tabIndex attribute 179 | 180 | ```jsx 181 | 182 | ``` 183 | 184 | > [!CAUTION] 185 | > Avoid using `tabindex` values greater than 0. Doing so makes it difficult for people who rely on assistive technology to navigate and operate page content. 186 | > Instead, write the document with the elements in a logical sequence. More about the use of tabIndex on [developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) 187 | 188 | ### Author: 189 | 190 | - [`Bluesky 👋 @AlaricBaraou`](https://bsky.app/profile/alaricbaraou.bsky.social) 191 | -------------------------------------------------------------------------------- /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 ``. 82 | 83 | There's a ready-made solution in drei: [useContextBridge](https://github.com/pmndrs/drei#usecontextbridge) which allows you to forward contexts provided above the `` to be consumed within it. 84 | 85 | You can see how it's used in the [react-three-a11y demo](https://n4rzi.csb.app) 86 | -------------------------------------------------------------------------------- /docs/advanced/beforehoverdefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/beforehoverdefault.png -------------------------------------------------------------------------------- /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/advanced/hoverdefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/hoverdefault.png -------------------------------------------------------------------------------- /docs/advanced/hoverdefaultbecause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/hoverdefaultbecause.png -------------------------------------------------------------------------------- /docs/advanced/hoverdefaultout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/hoverdefaultout.png -------------------------------------------------------------------------------- /docs/advanced/hoverfix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/hoverfix.png -------------------------------------------------------------------------------- /docs/advanced/hoverfixbecause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/advanced/hoverfixbecause.png -------------------------------------------------------------------------------- /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/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Quick start about how to make your WebGL accessible with @react-three/a11y and react-three-fiber 4 | nav: 0 5 | --- 6 | 7 |

8 | 9 |

10 | 11 | @react-three/a11y brings accessibility to webGL with easy-to-use react-three-fiber components: 12 | 13 | - Focus and focus indication 14 | - Tab index and keyboard navigation 15 | - Screen reader support and alt-text 16 | - Roles and cursor shapes 17 | - Descriptive links 18 | 19 | You can try a [live demo here](https://n4rzi.csb.app). 20 | Open it with your favourite assistive tech to test it out ! 21 | 22 | ```bash 23 | npm install @react-three/a11y 24 | ``` 25 | 26 | ## Quick overview to get started 27 | 28 | ### The A11yAnnouncer component 29 | 30 | First, place the A11yAnnouncer component next to the R3F Canvas component. This component is critical since it manage some screen-reader features. 31 | 32 | ```jsx 33 | import { Canvas } from '@react-three/fiber'; 34 | import { A11yAnnouncer } from '@react-three/a11y'; 35 | 36 | function App() { 37 | return ( 38 | <> 39 | 40 | 41 | 42 | ); 43 | } 44 | ``` 45 | 46 | ### Then wrap components you want to make accessible with the A11y component 47 | 48 | To add accessibility features to your scene you'll have to wrap components you want to make focusable with the `A11y` component: 49 | 50 | ```jsx 51 | import { A11y } from '@react-three/a11y' 52 | [...] 53 | 54 | 55 | 56 | ``` 57 | 58 | `MyComponent` can now receive focus. More accurately, the emulated "focus" will be handled at the `A11y` components which acts as a provider 59 | for children to access its state. But even if objects are focusable, nothing will be displayed or shown by default. 60 | 61 | ## Call function on focus 62 | 63 | The `focusCall` prop of `A11y` will be called each time this component receives focus (usually through tab navigation). 64 | 65 | ```jsx 66 | console.log("in focus")} ... /> 67 | ``` 68 | 69 | ## Call function on click / keyboard Click 70 | 71 | The `actionCall` prop of `A11y` will be called each time this component gets clicked, focused, keyboard activated etc. 72 | 73 | ```jsx 74 | console.log("clicked")} ... /> 75 | ``` 76 | 77 | ## Provide a description of the currently focused / hovered element 78 | 79 | When using the `description` prop in combination with the `role` prop, the `A11y` component will provide a description to the screen reader users on focus/hover. 80 | Optionally, you can also show the description to the user on hover by setting `showAltText={true}`. 81 | 82 | ```jsx 83 | // Reads "A rotating red square" to screen readers on focus / hover while also showing it on mouseover 84 | 85 | // Reads "Button, open menu + (description on how to activate depending on the screen reader)" to screen readers on focus / hover 86 | {someFunction()}} ... /> 87 | ``` 88 | 89 | ## The four roles of the A11y component 90 | 91 | Like in HTML, you can focus different kind of elements and expect different things depending on what you're focusing. 92 | 93 | #### Content 94 | 95 | ```jsx 96 | 97 | ``` 98 | 99 | Uses the `default` cursor. This role is meant to provide information to screen readers or to serve as a step for a user to navigate your site using Tab for instance. 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. 100 | 101 | [Read more about role content](/a11y/roles/content) 102 | 103 | #### Button 104 | 105 | ```jsx 106 | 110 | ``` 111 | 112 | Uses the `pointer` cursor. Special attributes: `activationMsg`. 113 | 114 | This role is meant to emulate the behaviour of a button or a toggleable button. It will display a cursor pointer when your cursor is over the linked 3D object. It will call a function on click but also on any kind of action that would trigger a focused button (Enter, Double-Tap, ...). It is also actionable by user using a screen reader. 115 | 116 | [Read more about role button](/a11y/roles/button) 117 | 118 | #### ToggleButton 119 | 120 | By using the role togglebutton, you'll emulate a button with two state that will have the `aria-pressed` attribute. 121 | You'll then be able to use the deactivationMsg property in addition to the usual description and activationMsg properties. 122 | 123 | ```jsx 124 | 129 | ``` 130 | 131 | Special attributes: `deactivationMsg` 132 | 133 | [Read more about role ToggleButton](/a11y/roles/togglebutton) 134 | 135 | #### Link 136 | 137 | ```jsx 138 | 139 | ``` 140 | 141 | Uses the `pointer` cursor. Special attributes: `href`. 142 | 143 | This role is meant to emulate the behaviour of a regular html link. It should be used in combination with something that will trigger navigation on click. 144 | 145 | > [!NOTE] 146 | > Don't forget to provide the href attribute as it is required for screen readers to read it correctly! - It will have no effect on the navigation, it's just used as information 147 | 148 | [Read more about role link](/a11y/roles/link) 149 | 150 | ## Screen Reader Support 151 | 152 | In order to provide informations to screen reader users and use this package at its full potential, fill the `description` prop of all your `A11y` components and use the appropriate `role` prop on each of them. 153 | 154 | ### Use of section 155 | 156 | For screen readers, it might be useful to provide additional information on how to use some unconventional UI. 157 | You can do it by wrapping the concerned part of your code relative to this UI in the A11ySection like so. 158 | 159 | ```jsx 160 | 164 | [...] 165 | 166 | ``` 167 | 168 | ## Access user preferences 169 | 170 | The A11yUserPreferences component is available in order to access user preferences such as 171 | 172 | - prefers-reduced-motion 173 | - prefers-color-scheme 174 | 175 | Take a look at [the A11yUserPreferences page](/a11y/access-user-preferences) or the [demo](https://n4rzi.csb.app) to see it in action and how to use it. The demo will adapt to your system preferences. 176 | 177 | ## Additional Features 178 | 179 | Use a custom tabindex with for your A11y components by providing a number to the tabIndex attribute 180 | 181 | ```jsx 182 | 183 | ``` 184 | 185 | > [!CAUTION] 186 | > Avoid using `tabindex` values greater than 0. Doing so makes it difficult for people who rely on assistive technology to navigate and operate page content. 187 | > Instead, write the document with the elements in a logical sequence. More about the use of tabIndex on [developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) 188 | -------------------------------------------------------------------------------- /docs/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/logo.jpg -------------------------------------------------------------------------------- /docs/react-three-a11y.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/react-three-a11y/fc70f2676723ffef85f3a0d7bdef7c463c22e950/docs/react-three-a11y.jpg -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /example/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "fluid": false 11 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mr-main-a11y-features-test", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "description": "This sandbox has been generated!", 6 | "keywords": [], 7 | "main": "src/index.js", 8 | "dependencies": { 9 | "@juggle/resize-observer": "3.2.0", 10 | "@pmndrs/branding": "0.0.4", 11 | "react": ">=18", 12 | "react-scripts": "5.0.1", 13 | "three": "*", 14 | "valtio": "0.6.0" 15 | }, 16 | "alias": { 17 | "react": "../node_modules/react", 18 | "react-dom": "../node_modules/react-dom", 19 | "@react-three/fiber": "../node_modules/@react-three/fiber", 20 | "three": "../node_modules/three", 21 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 22 | }, 23 | "devDependencies": { 24 | "parcel": "^2.5.0", 25 | "typescript": "4.5.2" 26 | }, 27 | "scripts": { 28 | "start": "parcel index.html", 29 | "build": "parcel build index.html" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | import { Canvas, useFrame, useThree } from "@react-three/fiber" 3 | import React, { useRef } from "react" 4 | import { A11y, useA11y, A11yAnnouncer, A11yUserPreferences, useUserPreferences, A11ySection, A11yDebuger } from "../../" 5 | import { ResizeObserver } from "@juggle/resize-observer" 6 | import { proxy, useProxy } from "valtio" 7 | import { Badge } from "@pmndrs/branding" 8 | 9 | const state = proxy({ dark: false, active: 0, rotation: 0, disabled: false, section: undefined }) 10 | const geometries = [ 11 | new THREE.SphereBufferGeometry(1, 32, 32), 12 | new THREE.TetrahedronBufferGeometry(1.5), 13 | new THREE.TorusBufferGeometry(1, 0.35, 16, 32), 14 | new THREE.OctahedronGeometry(1.5), 15 | new THREE.IcosahedronBufferGeometry(1.5), 16 | ] 17 | 18 | function ToggleButton(props) { 19 | const a11y = useA11y() 20 | return ( 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | function SwitchButton(props) { 29 | const a11y = useA11y() 30 | return ( 31 | <> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | function Floor(props) { 45 | return ( 46 | <> 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | function Nav({ left }) { 56 | const snap = useProxy(state) 57 | const viewport = useThree(state => state.viewport) 58 | const radius = Math.min(12, viewport.width / 2.5) 59 | return ( 60 | { 65 | state.rotation = snap.rotation + ((Math.PI * 2) / 5) * (left ? -1 : 1) 66 | state.active = left ? (snap.active === 0 ? 4 : snap.active - 1) : snap.active === 4 ? 0 : snap.active + 1 67 | }} 68 | disabled={snap.disabled}> 69 | 70 | 71 | ) 72 | } 73 | 74 | function Diamond({ position, rotation }) { 75 | const a11y = useA11y() 76 | return ( 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | function Shape({ index, active, ...props }) { 85 | const snap = useProxy(state) 86 | const vec = new THREE.Vector3() 87 | const ref = useRef() 88 | const { a11yPrefersState } = useUserPreferences() 89 | useFrame((state, delta) => { 90 | if (snap.disabled) { 91 | return 92 | } 93 | if (a11yPrefersState.prefersReducedMotion) { 94 | const s = active ? 2 : 1 95 | ref.current.scale.set(s, s, s) 96 | ref.current.rotation.y = ref.current.rotation.x = active ? 1.5 : 4 97 | ref.current.position.y = 0 98 | } else { 99 | const s = active ? 2 : 1 100 | ref.current.scale.lerp(vec.set(s, s, s), 0.1) 101 | ref.current.rotation.y = ref.current.rotation.x += delta / (active ? 1.5 : 4) 102 | ref.current.position.y = active ? Math.sin(state.clock.elapsedTime) / 2 : 0 103 | } 104 | }) 105 | return ( 106 | 107 | 108 | 109 | ) 110 | } 111 | 112 | // const ResponsiveText = () => { 113 | // const { viewport } = useThree() 114 | // const posX = useControl("posX", { type: "number", value: 0, min: -20, max: 20 }) 115 | // const posY = useControl("posY", { type: "number", value: 0, min: -20, max: 20 }) 116 | // const posZ = useControl("posZ", { type: "number", value: 0, min: -20, max: 20 }) 117 | // const color = useControl("color", { type: "color", value: "#EC2D2D" }) 118 | // const fontSize = useControl("fontSize", { type: "number", value: 16.5, min: 1, max: 100 }) 119 | // const maxWidth = useControl("maxWidth", { type: "number", value: 90, min: 1, max: 100 }) 120 | // const lineHeight = useControl("lineHeight", { type: "number", value: 0.75, min: 0.1, max: 10 }) 121 | // const letterSpacing = useControl("spacing", { type: "number", value: -0.08, min: -0.5, max: 1 }) 122 | // const textAlign = useControl("textAlign", { 123 | // type: "select", 124 | // items: ["left", "right", "center", "justify"], 125 | // value: "justify", 126 | // }) 127 | // return ( 128 | // 139 | // LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT 140 | // 141 | // ) 142 | // } 143 | 144 | function Carroussel() { 145 | const viewport = useThree(state => state.viewport) 146 | const snap = useProxy(state) 147 | const group = useRef() 148 | const radius = Math.min(6, viewport.width / 5) 149 | const { a11yPrefersState } = useUserPreferences() 150 | useFrame(() => { 151 | if (a11yPrefersState.prefersReducedMotion) { 152 | group.current.rotation.y = snap.rotation - Math.PI / 2 153 | } else { 154 | group.current.rotation.y = THREE.MathUtils.lerp(group.current.rotation.y, snap.rotation - Math.PI / 2, 0.1) 155 | } 156 | }) 157 | return ( 158 | 159 | {["sphere", "pyramid", "donut", "octahedron", "icosahedron"].map((name, i) => ( 160 | 168 | ))} 169 | 170 | ) 171 | } 172 | 173 | const CarrousselAll = () => { 174 | const snap = useProxy(state) 175 | 176 | return ( 177 | <> 178 | 181 |