UI State visualization
State handling is a crucial part of building a user interface. It is important to provide a good user experience when the application is in different states such as loading, error, or empty. This document provides best practices for handling different states in the user interface.
A. Error State
The error state should be the first thing to handle when building a user interface. It is important to provide a clear and helpful error message to the user when something goes wrong. There are three levels of error handling in the user interface: page-level error, component-level error, and action error.
Page-level error
We should show clear and centralized error (e.g.
error.tsx) and it is the default error bounding for display. We should have enough message for the error details and also the way to contact support. It should align to the default 404 page error.
Component-level error
Use the correct UI element for the type of error

For using existing, let’s reference wemakeapp-ui project with the
ErrorBox component:
Action error
In most cases, we should use Component-level error instead of action error. If you are planning to adopt one, try to align with the overall design.

B. Loading State
Page-level loading
For better user experience, we make use of Server-side component to avoid page level loading. However, if there is a case to display page-level loading, please checkout
wemakeapp-ui project’s LoadingState component with the centralized spinner example.Component-level loading
Skeleton’s like loading is the best practice in modern UI development. With the combination of the
Skeleton component with shadcn/ui and also the Pulse animation of Tailwind, it would come with a better UI expectation while the user is waiting.
Example: Tab-based skeleton loading
{ loading ? ( <div className="flex gap-2"> <Skeleton className="h-8 w-12"></Skeleton> <Skeleton className="h-8 w-12"></Skeleton> <Skeleton className="h-8 w-12"></Skeleton> </div> ) : ( <Tabs value={activeTab} onValueChange={(value) => { if (isValidVisibilityToggle(value)) { toggleVisibility(value) } }} className="print:hidden" > <TabsList> <TabsTrigger value="all">All</TabsTrigger> <TabsTrigger value="en">English</TabsTrigger> <TabsTrigger value="fr">French</TabsTrigger> </TabsList> </Tabs> ) }
Link-level loading
If you want to detect loading from
<Link... , you can use useLinkStatus from Next.js, or you can use nextjs-toploader to apply globally.C. Empty State
Page-level empty
It would be basically like the default 404 view, checkout Next.js’s notFound route example.
Component-level
Below are the expected elements and example when there is no data for such view.
