How to Make React File Upload Progress and Errors Accessible

Posted on
How to Make React File Upload Progress and Errors Accessible

If you’ve already built a React file upload flow, it probably looks good and works well on a fast connection. But there’s one important area that many tutorials don’t cover: accessibility.

For example, a drag-and-drop area might only work with a mouse, a progress bar might update on the screen without giving any updates to screen reader users, or an error message might appear in red without clearly showing which field caused the problem.

This isn’t a general guide to accessibility. Instead, we’ll focus on improving a React upload flow you already have, whether it’s built with Filestack’s React SDK or your own custom components.

You’ll learn how to make the drop zone, upload progress, and error messages easier to use for people who can’t see the screen or can’t use a mouse.

If you’re working with large-file performance instead, our guide on pausing and resuming large file uploads in React covers that topic. And if you want to learn about the same accessibility issues without focusing specifically on React, HTML file upload accessibility is also worth reading.

Key Takeaways

  • Drag-and-drop doesn’t work with a keyboard by default, so every drop zone should also have a keyboard-friendly option.
  • A visual progress bar shows upload progress to sighted users, but screen reader users need ARIA updates to know what’s happening.
  • Don’t announce every 1% change to screen reader users. Give progress updates at reasonable intervals to avoid too many announcements.
  • Error messages should be properly connected to the file or field that caused the error, not just displayed nearby.
  • Tools like axe and Lighthouse can find issues such as missing labels, but testing with a real screen reader is important to make sure the whole upload experience is easy to understand.

With these key points in mind, let’s start with one of the most common accessibility problems in file uploads: drag-and-drop.

Why Drag-and-Drop Upload Zones Are an Accessibility Blind Spot

Drag-and-drop feels simple and modern, but it can be difficult to use for people who don’t use a mouse or trackpad.

The main problem is that dragging and dropping is a mouse-based action. There isn’t a built-in keyboard version of dragging a file into a drop zone. If your drop zone only uses onDragOver and onDrop events, keyboard users may not be able to use it at all.

Most drop zones also use visual changes to show when they’re active, such as changing the border when a file is dragged over them. A screen reader can’t detect or announce this visual change on its own.

You might think adding a “click to browse” option solves the problem. It helps, but that option also needs to be accessible with a keyboard, have a clear label, and use the same progress and error handling as the drag-and-drop option.

Diagram showing two versions of drag-and-drop zone

The W3C’s guidance on dragging movements explains that actions that depend on dragging should also have a simpler alternative that doesn’t require a drag gesture.

Once you have that alternative, the next step is making sure both the fallback and the drop zone are easy to use with a keyboard.

Making the Drop Zone Keyboard-Operable

Making a drop zone keyboard-friendly is less about ARIA and more about making sure users can reach and use it without a mouse.

Reachable and Triggerable via Keyboard

A native <input type="file"> already works with a keyboard. Users can tab to it and press Enter or Space to open the file picker.

Problems usually happen when you create a custom drop zone using a styled <div> with a hidden file input. If the <div> isn’t keyboard-accessible, users may tab past it without knowing it’s there.

Here’s a simple example:

// A simple, keyboard-reachable drop zone

function DropZone({ onFilesSelected }) {

const inputRef = useRef(null);

const openFilePicker = () => inputRef.current.click();

const handleKeyDown = (event) => {

// Enter or Space should behave like a click

if (event.key === 'Enter' || event.key === ' ') {

event.preventDefault();

openFilePicker();

}

};

return (

<div

role="button"

tabIndex="0"

onClick={openFilePicker}

onKeyDown={handleKeyDown}

onDrop={(e) => {

e.preventDefault();

onFilesSelected(e.dataTransfer.files);

}}

onDragOver={(e) => e.preventDefault()}

className="drop-zone"

>

<p>Drag a file here, or press Enter to choose one</p>

<input

ref={inputRef}

type="file"

hidden

onChange={(e) => onFilesSelected(e.target.files)}

/>

</div>

);

}

In this example, tabIndex="0" lets keyboard users reach the drop zone. The handleKeyDown function also lets them press Enter or Space to open the file picker.

Visible Focus Indicators

Making the drop zone keyboard-accessible isn’t enough. Users also need to clearly see when it has keyboard focus.

Avoid removing the default focus outline with outline: none unless you replace it with another clear focus style.

For example:

.drop-zone:focus-visible {

outline: 3px solid #EF4A25;

outline-offset: 2px;

}

Now keyboard users can reach the drop zone, open the file picker, and clearly see when the drop zone is focused.

Once the file is selected, the next step is making sure users can also understand how the upload is progressing.

Announcing Upload Progress to Screen Reader Users

A progress bar might look clear on the screen, but that doesn’t mean every user knows what’s happening.

Why a Progress Bar Alone Isn’t Enough

A <progress> element or a styled <div> can visually show how much of a file has uploaded. But screen reader users may not know that the progress is changing unless those updates are announced.

Without these announcements, they may not know whether the upload has started, how far it has progressed, or when it has finished.

Using ARIA Live Regions Without Creating Noise

An ARIA live region lets screen readers know when important content on the page changes. For an upload, you can use it to announce progress as the percentage increases.

However, you shouldn’t announce every single percentage change. Hearing “1%… 2%… 3%…” can quickly become distracting. Instead, announce progress at larger intervals, such as every 10%.

You can learn more about how this works in MDN’s guide to ARIA live regions.

Here’s a simple example:

function UploadStatus({ progress, isComplete }) {

const [announcement, setAnnouncement] = useState('');

useEffect(() => {

if (isComplete) {

setAnnouncement('Upload complete.');

return;

}

// Only announce at 10% steps, not every single percent

if (progress % 10 === 0) {

setAnnouncement(`Upload ${progress}% complete.`);

}

}, [progress, isComplete]);

return (

<div>

<progress value={progress} max="100" />

{/* This div is what screen readers listen to */}

<div aria-live="polite" className="visually-hidden">

{announcement}

</div>

</div>

);

}

Diagram showing how an ARIA live region announces upload progress

Announcing Completion Clearly

When the upload finishes, give users a clear message such as “Upload complete.”

Don’t rely only on the final “100%” progress update. A separate completion message makes it clear that the upload has successfully finished.

But progress updates are only one part of the experience. You also need to make sure users clearly understand when an upload fails and what caused the problem.

Making Error States Accessible

Error messages are another important part of an accessible upload flow. A common problem is that the error message appears near the file input visually, but isn’t properly connected to it for screen reader users.

Associating Errors with aria-describedby

Putting an error message below a file input makes the connection clear to someone looking at the screen. But a screen reader may not know that the error belongs to that input.

You can use aria-describedby to connect the file input to its error message. This helps screen readers understand and announce the relationship between them.

Diagram showing connecting a file input to its error message

function FileInputWithError({ error }) {

return (

<div>

<input

id="resume-file"

type="file"

aria-describedby={error ? 'resume-file-error' : undefined}

aria-invalid={Boolean(error)}

/>

{error && (

<p id="resume-file-error" role="alert">

{error}

</p>

)}

</div>

);

}

Here, aria-describedby connects the input to the error message. aria-invalid tells assistive technology that the input currently has an error.

The error also uses role="alert", which helps screen readers announce the message when it appears.

Announcing Errors as They Happen

Don’t wait until the user submits the form to announce an error.

For example, if someone selects a file that’s too large or uses the wrong file type, show and announce the error as soon as the file is rejected. This lets the user know immediately what went wrong.

Writing Error Messages That Make Sense Out of Context

Avoid unclear messages such as “Error: invalid input.” They don’t explain what went wrong or how to fix it.

Instead, use a specific message such as “This file is 45MB, but the limit is 25MB.” This tells the user exactly what the problem is and helps them choose a suitable file.

Once your drop zone, progress updates, and error messages are accessible on their own, the next step is bringing them together in a single component.

Implementing This in a React Component

Now, let’s bring everything we’ve covered into one React upload component.

This is a simple example. A real-world uploader will usually have more file-handling logic, but the accessibility setup will remain similar.

function AccessibleUploader() {

const [progress, setProgress] = useState(0);

const [status, setStatus] = useState('idle'); // idle | uploading | success | error

const [error, setError] = useState(null);

const [liveMessage, setLiveMessage] = useState('');

const successRef = useRef(null);

const handleFiles = (files) => {

const file = files[0];

if (file.size > 25 * 1024 * 1024) {

setStatus('error');

setError('This file is larger than the 25MB limit.');

return;

}

setStatus('uploading');

setError(null);

// Upload logic (e.g. calling Filestack's upload method) would go here,

// calling setProgress(...) as it reports progress.

};

useEffect(() => {

if (status === 'success') {

setLiveMessage('Upload complete.');

// Move focus somewhere sensible once the upload finishes

successRef.current?.focus();

} else if (status === 'uploading' && progress % 10 === 0) {

setLiveMessage(`Upload ${progress}% complete.`);

}

}, [status, progress]);

return (

<div>

<DropZone onFilesSelected={handleFiles} />

<div aria-live="polite" className="visually-hidden">

{liveMessage}

</div>

{status === 'uploading' && <progress value={progress} max="100" />}

{status === 'error' && (

<p id="upload-error" role="alert">

{error}

</p>

)}

{status === 'success' && (

<p tabIndex="-1" ref={successRef}>

Your file uploaded successfully.

</p>

)}

</div>

);

}

 

What this code does:

  • Tracks the upload progress using the progress state.
  • Tracks whether the upload is idle, uploading, success, or error.
  • Checks the file size before starting the upload and shows an error if the file is larger than 25MB.
  • Uses an ARIA live region to announce upload progress to screen reader users.
  • Announces progress every 10% instead of announcing every small change.
  • Announces when the upload is complete.
  • Shows an error message with role="alert" if something goes wrong.
  • Moves keyboard focus to the success message after the upload finishes.
  • Uses the accessible DropZone component created earlier for selecting files.

Managing the Live Region Without Over-Announcing

In the above example, all screen reader announcements are stored in one state variable called liveMessage.

This makes it easier to control when a new message is announced. Instead of announcing every small progress change, the component only updates the message at useful points, such as every 10%.

Focus Management After Upload Finishes or Fails

After an upload finishes, you can move keyboard focus to the success message using tabIndex="-1" and .focus().

This helps screen reader users immediately understand that the upload has finished instead of leaving their focus on the drop zone.

The same idea can also be used for errors. If the upload fails, you can move focus to the error message so the user knows what happened and what they should do next.

Managing states such as uploading, success, and error becomes even more important as your upload component grows. The patterns discussed in how you can fix the biggest problem with React file upload can help when you’re working with a more complex upload flow.

Once the code is in place, the next step is testing it with the same tools and interactions your users rely on.

Testing With a Real Screen Reader, Not Just a Linter

Automated accessibility tools are useful, but a clean report doesn’t always mean your upload flow is fully accessible.

What Automated Tools Catch

Tools like axe and Lighthouse can find common accessibility problems, such as missing labels, missing alt text, poor color contrast, or inputs without accessible names.

It’s a good idea to run these tools regularly because they can quickly catch basic issues. WebAIM’s introduction to ARIA is also a useful resource for understanding how ARIA roles and attributes should work.

What Only Manual Testing Catches

Automated tools can’t tell you everything. For example, they can’t always tell whether progress updates are announced at the right time, whether an error message makes sense when heard without seeing the screen, or whether the keyboard navigation feels natural.

That’s why you should also test the upload flow with a real screen reader such as NVDA or VoiceOver. Try going through the entire upload process without using a mouse.

Check whether you can select a file with the keyboard, understand the upload progress, hear error messages clearly, and know when the upload has finished.

Using both automated tools and manual testing gives you a much better idea of how accessible your upload flow really is.

With testing covered, let’s look at some common mistakes to avoid when building an accessible file upload experience.

Best Practices and Common Pitfalls

Here are a few important things to remember when making your React file upload accessible.

Best Practices

  • Make sure the upload works with a keyboard first, then add drag-and-drop support.
  • Announce upload progress at reasonable intervals, such as every 10–20%, instead of every small change.
  • Use clear messages for both successful and failed uploads.
  • Move focus to the success or error message when the upload finishes so users know what happened.
  • Test the complete upload flow with a real screen reader before considering it finished.

Common Pitfalls

  • Don’t use only a colored border or icon to show an error. Include a clear text message and connect it to the correct input.
  • Don’t remove the default focus outline unless you replace it with another visible focus style.
  • Don’t update an aria-live region on every progress change. Too many announcements can make the experience difficult to follow.
  • Don’t assume a “click to browse” option is automatically accessible. It still needs clear labeling and keyboard support.
  • Don’t rely only on automated accessibility tools. Use them as a first check, then test the experience manually with a screen reader.

With these best practices and common mistakes in mind, it’s also worth looking at how newer SDK updates can support the React upload flows you’re building.

What’s New in Filestack’s React Support

If you’re using Filestack’s React SDK for your upload flow, it’s worth keeping up with the latest updates.

Filestack React SDK v7.0.0 release brought several improvements, including full TypeScript support, React 19 support, and better compatibility with frameworks like Next.js, Vite, and Remix.

As the SDK continues to change, the accessibility features you add should continue to work with newer versions. Future-proofing your React file uploader is a useful next read for keeping your uploader up to date as React and the SDK evolve.

Whether you’re using Filestack or your own React components, the main accessibility principles stay the same.

Conclusion

You don’t need to rebuild your React upload flow to make it more accessible. Most of the work is about fixing small things that are easy to miss when testing only with a mouse and screen.

Make sure users can reach and use the drop zone with a keyboard, provide clear announcements for upload progress and errors, and connect error messages to the correct fields.

With these changes, your existing React upload component becomes easier to use for more people without changing the experience for other users.

FAQ

Is a drag-and-drop file upload zone accessible by default?

No. Drag-and-drop doesn’t have a built-in keyboard option. Your drop zone should also provide an accessible alternative, such as a clearly labeled file input that users can reach and use with only a keyboard.

How do I announce upload progress to screen reader users in React?

Use an ARIA live region to announce upload progress to screen reader users, but don’t announce every percentage change. Instead, give updates every 10–20% to avoid too many announcements. When the upload finishes, announce a separate message like “Upload complete.”

How should error messages be associated with the file that failed to upload?

Use aria-describedby to connect the error message to the correct file or field. Simply placing the error message nearby isn’t enough because screen readers may not understand that they’re related.

Is automated accessibility testing enough to confirm an upload flow is accessible?

No. Tools like axe or Lighthouse can find issues such as missing labels and other accessibility problems, but they can’t check everything. They can’t tell whether progress updates are announced at the right time or whether an error message makes sense without seeing the screen. That’s why you should also test your upload flow manually with a real screen reader.

Does having a file-picker fallback next to a drag-and-drop zone make the whole flow accessible?

Not automatically. The fallback also needs to be clearly labeled and accessible with a keyboard. It should also use the same progress updates and error announcements as the drag-and-drop option.

Read More →

Ready to get started?

Create an account now!