Performance is a priority at GitHub, and the first approach is to improve the speed of our processes. However, there are times when we can't avoid making users wait. In these cases, maintaining visibility of system status through loading states can help users feel stable and in control.
Without indicating that something is loading, users may think a process has silently failed. This can lead to confusion and frustration.
Some of our components already have loading states built in. Rely on component-specific patterns before designing your own loading experiences.
For example:
Loading indicators reassure users that their request is actively being executed, and can sometimes be used to set an expectation for how long they'll be waiting. They may even reduce the users' perception of time.
Animation in loading indicators helps reassure the user that the process is active and the system isn't frozen. However, animation can become distracting or overwhelming. Loading indicators' animation should be subtle, and we should avoid letting a page get overtaken by too many of them.
Determinate loading indicators may be used when the progress or duration of a process is known or can be estimated. This type of loading gives the user an idea of how much longer they'll have to wait.
Determinate loading indicators work best for processes that are likely to take longer (approximately 3 or more seconds). Shorter waiting times don't give the user enough time to process the information conveyed by a determinate loading indicator.
Indeterminate loading indicators may be used when the progress or duration of a process is variable or unknown.
This type of loading gives the user a sense that something is happening, but not how long it will take.
For large areas of loading content, the loading content may be replaced by a vague representation of the content. This shows users the general shape of the page and could make them perceive the real content as loading faster.
Skeleton loaders may be used when we can approximate the visual shape the loaded content will create on the page.
The tree view and data table components have these states implemented.
An animated icon or illustration may be used as a "branding moment" for special loading states. This should be used sparingly.
Text may accompany any of the above loading indicators to give the user more context about the process.
The following is meant as general guidance since we can't accurately predict how long a process will take. It's difficult to give even a rough estimate of how long a process will take due to unknown variables such as network speed, device performance, and server load.
Less than 1 second: Don't show a loading state. Seeing a loading indicator flash on the screen could be distracting and make the product feel slower than it is.
1–3 seconds: Use an indeterminate loading state. The user won't have enough time to process the information in a determinate loading state, and potentially cause frustration or confusion about missing information.
3–10 seconds: Use a determinate loading state if possible to keep the user informed about why they're waiting so long.
More than 10 seconds: Use a determinate loading state and avoid blocking other interactions by treating the process as a background task if possible. This reduces the interruption caused by a lengthy process, and could make wait times feel shorter if a user can shift their focus to other tasks. For example, the user can still interact with a pull request while they're waiting for CI actions to run.
Process initiated: The moment just before loading begins. Usually initiated by a user action such as clicking a button or submitting a form
Process in progress: The process is underway. This is the actual loading state.
Process complete successfully: The process has finished successfully, and the user can continue with their task. If the user isn't navigated to a new context when the task has completed, this step may include some indication of success, such as a green checkmark or a success message.
Process failed: There was an error, and the process could not be completed. This step should include an error message that explains what went wrong and what the user's next time may be. For example, if the user can retry the process, include a button to do so.
Whenever possible, show each item in a collection as soon as it loads. Don't wait for every item in the collection to load before showing the items.
Show content as soon as it's loaded.
Don't wait for all content in a collection to be loaded before showing it.
Whenever possible, prioritize the most important pieces of data to load first. This shows the user the most important content first and gives them a chance to interact with things sooner. This could be especially helpful for somebody on a slow network.
A loading indicator should be placed nearest to the content they are standing in for. Avoid creating a jarring layout shift when the loaded content replaces the loading indicator.
Replace loading indicators with loaded content or error feedback as soon as possible to avoid burdening users with unnecessarily long wait times.
Some processes navigate you to a new page once they've completed, and use an interstitial loading page as a transition. This should be done sparingly because loading a new page is a bigger interuption than showing a loading indicator in context on the page.
Interstitial loading screens are useful for:
Position your loading state in the center of the region, but ensure it's still visible within the viewport.
To avoid information overload and visual noise, consider replacing a series of adjacent loading indicators with a single loading indicator.
Reduce the number of loading indicators shown on a page.
Don't show a loading indicator for every piece of loading content on the page.
If sequential groups or navigation links are unable to load, replace both groups with a single loading indicator and a single error message above the nav list.
If non-sequential groups of navigation links are in a loading state, preserve the group headings and replace the contents of each group with a single loading indicator. If loading fails, show a single error message above the nav list.
If some links in the right side sheet cannot be loaded, default to showing each nav list item in an inert state. The users should still be able to hover, click, and focus inert items, but they don't link to anywhere. Optionally, a tooltip may be displayed explaining that the navigation item is unavailable. An error banner is not necessary for this case.
If we don't know specifically which links failed to load (for example, some users will never see the "Your enterprises" link), then hide the links and show a generic error message above the nav list.
Default to replacing each loading navigation item with the item in a loading state. If we know what the link's label is, show it. If we don't know what the link's label is, show generic loading text in its place.
It is ok to show sequential navigation items in a loading state to avoid a layout shift. However, sequential groups of navigation items should be replaced with a single loading indicator as mentioned in the previous section.
Loading indicators are visual and need to be accessible to assistive technologies. This can be done with a text element we associate using aria-labelledby
, or attributes such as alt
(on an img
element) and title
(on a svg
element). The loading text element may be visually hidden.
You may default to a generic label such as "loading", but try to be specific when possible. For example, when a spinner replaces status checks in a pull request, it could have the label "loading status checks".
Communicate when a process has been initiated, when it's in progress, and when it has been completed or if it has failed.
There are many cases where it will be obvious that a process has completed or failed, and we won't need an additional announcement. For example, a page load or a new element getting focused.
For other cases, the status will need to be manually conveyed to assistive technologies. Use a role="status"
message or put content in an aria-live
element that communicates the status of a process.
If the user needs to wait for filtering to complete, a screen reader should announce how many results were returned from filtering. For example: "No items match the filter", or "5 items match the filter".
To ensure that assistive technology is informed about the filter update, put the message in an element that has a role
of "status"
. The element with role="status"
must always be rendered, not just when the message should be announced.
Implementations of this pattern can be seen in the following examples:
If an aria-live
region of a page is being updated, set aria-busy="true"
on the live region until the updates are complete. Then, set aria-busy="false"
. This prevent assistive technologies from announcing updates until the updates are complete.
Follow existing focus patterns. When focus needs to move, it should go to the most logical and predictable. Before implementing your own focus management strategy for a loading state, confirm that the component hasn't already implemented it. For example, when a dialog closes, focus is automatically moved back to the control that opened the dialog.
If the completed process results in navigating to a new page, just rely on the browser's default focus behavior for new pages.
If the process fails and a validation message is shown, move focus to the first focusable element in the error message. See the form validation guidelines for more info.
If the process loads new content, move focus to first focusable element in the newly loaded content. This pattern is also described in the tree view focus guidelines.
You may disable form controls after the user submits the form to avoid confusion over whether changes that were made during the submission process will be saved.
However, the button that initiates the a loading process may not be semantically disabled.
When implementing a "loading" button state, don't remove the button from the DOM or pass the disabled
attribute. Doing so would make it impossible to tab to the button. If the button was just focused and activated, it would reset focus. Resetting focus would disrupt the keyboard navigation flow, and creates a confusing experience for assistive technologies such as screen readers.
Once the button is activated (and is in a loading state), it get the attribute aria-disabled="true"
.
A separate, visually hidden element should be rendered outside of the <button>
with a message to communicate the loading status. For example, "Saving profile".
This message should be in an ARIA live region, using aria-live="polite"
. The live region must be present on page load, but the message inside the live region should only be rendered while the button is in a loading state.
If an error prevents process from being completed, focus should be brought to an <h2>
(or next relevant heading) of the error banner.
Animated loading indicators help reassure the user that the system isn't frozen, and should not be disabled when for users that prefer reduced motion. However, the animation in loading indicators should be kept as subtle as possible. Large, flashy animations would be harmful to users with sensitivity to motion.