How Updog Makes Its Importer Accessible
Some people use an app with the keyboard alone. Others hear it through a screen reader, software that turns page elements into speech. Everything a web app does has to be within reach of both.
The Web Content Accessibility Guidelines, usually shortened to WCAG, describe what that takes. Version 2.2 sorts its requirements into three levels. Level A covers the basics, level AA covers the set that buyers, procurement teams, and public bodies normally ask for, and level AAA goes further than most products can meet across a whole interface. Level AA is the target Updog builds toward.
The work starts below the interface. Updog is made of components we own, most of the importer is ordinary HTML built from those components, and one part is a data grid painted on a canvas. Each layer carries its own share of the job.
What level AA asks for
The requirements come down to a handful of things a person can check.
Everything has a name and a role. A screen reader announces what a control is and what it is called. A button says it is a button and says what it does. A field says what it holds.
Everything works from the keyboard. Any action available to a mouse is available to Tab, the arrow keys, Enter, and Escape, and focus never gets stuck anywhere it cannot leave.
Focus is visible. The element that will receive the next keypress is marked clearly enough to find on screen.
Contrast is high enough to read. Normal text holds a ratio of 4.5 to 1 against its background. Large text, icons, and the borders that show where a field is hold 3 to 1.
The layout survives being resized. Text scales to 200 percent and content reflows into a column about 320 pixels wide without losing anything.
Errors are explained in words. A red border alone leaves out anyone who cannot see the red.
An automated rule checker can measure most of these. Whether a name means anything to the person hearing it is a separate question, and answering it takes someone listening. Updog does both.
A UI kit of our own
Updog ships its own components, and the importer and the editor are built from them. Buttons, inputs, selects, combo boxes, date pickers, dropdowns, menus, modals, drawers, tooltips, and tables all come from that kit.
Building a component library is more work than installing one. Adopting somebody else's kit means adopting their role model, their focus behavior, and their release schedule, and a naming decision buried inside it becomes something to work around. Owning the kit puts every accessible name, every keyboard interaction, and every color token in one place under our control, and a change reaches the whole product at once.
That control shows up in ordinary details. A label is wired to its control by id, so clicking the label moves focus and a screen reader reads the two together. A control takes its name from the label wired to it rather than from the placeholder inside it, so a filter announces the field it belongs to. A field holding a pair of controls, such as the minimum and maximum of a range filter, becomes a labelled group where each control keeps a name of its own, so one announces "Age, minimum" and the other "Age, maximum".
Every component has a story in Storybook, and every story is scanned for accessibility rule violations as part of the normal test run. Unit tests pin the roles and names of the dropdown and everything built on top of it, so later refactoring cannot change what a screen reader hears without a test failing.
Everything outside the grid
The importer has two halves. The upload wizard, the filter panel, the modals, the menus, and the cell editors are regular DOM elements. The data grid is a canvas. The DOM half inherits its accessibility from the kit, which is the point of owning the kit.
Every control on those screens carries a name a screen reader can read, and every step of the wizard announces which step is active. Anything reachable with a mouse is reachable with Tab and the arrow keys, and panels that close drop out of the tab order, so a screen reader reads only what is open.
The grid on a canvas
A browser cannot hold a million rows as elements and still scroll inside a frame budget, so the grid paints pixels instead, which is the rendering side of the decision. A canvas is one element with an image inside it. The roles, the names, and the position information an HTML table gives assistive technology for free are absent, so the grid supplies all of them itself.
The way it does that is a second copy of the grid built from real elements. Beside the canvas sits a table holding only the rows currently in view, roughly twenty of them at a normal window size, and scrolling rebuilds it to match what the canvas has painted. A screen reader reads that table and never touches the canvas.
This table has to be invisible on screen and still available to a screen reader, which rules out the usual ways of hiding something. Setting an element to display none takes it out of the page, and marking it hidden to assistive technology takes it out of the accessibility tree, the structure a browser builds for a screen reader to read. So the table stays in the layout inside a box one pixel across with its contents clipped away. Nobody sees it, and a screen reader finds it in the normal place.
Twenty rows would describe the file wrongly if the numbers stopped there. The grid states how many rows and columns the dataset holds, and every row and cell in the table states where it sits inside that dataset. A screen reader can then say row 4,318 of 40,000 while only twenty rows exist on the page. ARIA has attributes for this, and a virtualized grid is what they were written for.
Those numbers track what the user is looking at rather than what the file contains. Filter the grid down to one row and the count becomes one. Open the bin and the count covers the deleted rows. Pin a column to the left and the header order in the table moves with it.
Rebuilding that table on every scroll frame would compete with the canvas for the main thread and cost frames, so it waits until 150 milliseconds after scrolling stops. Keyboard movement is different. An arrow press past the bottom edge scrolls the grid, and waiting there would leave the new row missing for a moment, so a keypress rebuilds the table straight away.
Moving around the grid also has to be heard. A small area of the page holds a line of text that a screen reader reads whenever it changes, and the grid writes into it. Moving to a cell writes the row number, the column name, and the value. Selecting rows or columns writes how many. Counts go through the plural rules of the language being read, so English gets "1 row selected" and "3 rows selected", and Arabic gets all six of its plural forms.
A cell that fails a validation rule shows a red border on the canvas, which says nothing to a screen reader. The same cell in the table is marked as invalid and carries the message with it, and that message is spoken when focus lands on the cell.
What the keyboard reaches
A grid holding a million cells cannot give each one a tab stop, because crossing it would take a million presses. So the whole grid is a single tab stop. Tab moves focus onto it, and from there the arrow keys move a cursor from cell to cell, which is how a spreadsheet already works.
Arrows move the cell cursor and Shift + ↑ extends the selection, while Cmd + ↑ jumps to the edge of the data, with Ctrl standing in for Cmd away from a Mac. Shift + Space takes the whole row the cursor sits on, and Cmd + Space takes the column, bound the way Excel and Google Sheets bind them. Through all of it the cursor keeps its place, so Enter still opens the editor on the cell you were on. In a right-to-left layout, left and right swap.
Enter or F2 opens the editor on the focused cell, and typing a character opens it and starts with that character. Esc closes it again. Copy, cut, paste, undo, redo, select all, and clearing a cell all sit on the shortcuts a spreadsheet uses.
The context menu opens with Shift + F10, or with the dedicated Menu key. Mac keyboards carry neither, so Cmd + Shift + \ opens it instead, which is the binding Google Sheets uses. What opens follows the selection, so a row selection opens the row menu with Clear row, Duplicate row, and Delete row on it.
Closing a menu, an editor, or a popup sends focus back to the grid while the closing animation is still running. Someone who picks "Insert row above" and starts typing straight away keeps the first characters.
An editor opens on top of the canvas with no label beside it and no cell element behind it, so it builds a name out of the column and the row and announces "Edit First Name, row 1". The list inside a select editor takes its name from the field it belongs to.
The rest of the keys, and the places the grid parts from Excel, sit in the keyboard shortcuts reference.
Color, contrast, and motion
Text has to stand out from what sits behind it, and level AA puts that at 4.5 to 1 for normal text. Every text token is measured against the surface it lands on. For example, the gray used for placeholder text measures 4.98 to 1 on the base surface and 4.68 to 1 on the raised one, and that single token covers the placeholders of the text field, the select, the combo box, and the date picker.
A keyboard user needs to see where focus has gone, and a mouse user already knows. So controls draw an outline when focus arrives from the keyboard and stay quiet under a click.
Animation makes some people ill, and the operating system already knows who they are. With reduced motion switched on, the animated controls stop animating, and the dropdown and the context menu drop their transitions.
The colors are the one part we cannot measure for you. Updog is themed with CSS custom properties and your palette replaces ours, so your own tokens need checking against the same 4.5 to 1 threshold.
How we test
Every component has a story, and every story is scanned with axe, an accessibility rule checker, inside a headless Chromium browser as part of the normal test command. A violation fails the run. Content rendered into a portal is scanned along with the rest, which is where popups usually slip past a scanner.
The importer is scanned the same way. Playwright opens a state and checks the page against WCAG 2.2 level A and AA rules, working through every wizard step, all five cell editors, each menu, modal, and panel the editor can open, and the editor itself in readonly and right-to-left form.
A scanner reads the colors a browser has computed, and a color halfway through a fade belongs to neither end of it. So every scan waits for the animations on the page to finish before it measures anything.
People listen to it as well. Every state of the importer gets a manual pass with VoiceOver on macOS.
Where we aim
Level AA is the standard we write against and measure by, on every change to the code. Formal conformance has its own paperwork, an independent audit or a Voluntary Product Accessibility Template, and as of August 2026 Updog holds neither. Accessibility is part of how we build and test the product, and the work of getting closer to that level continues.