On this page
Import
import {
Alert,
Badge,
Button,
Column,
Grid,
List,
Page,
PageHeader,
placeholders,
Row,
Scaffold,
SearchField,
Select,
Surface,
Table,
Text,
TopBar,
} from '@envsemble/envui';
Usage
An empty state is a Column of Text and a Button, with no component of its own:
- the icon in a muted Text;
- the heading as Text with
variant="title"at the level the outline needs, such asrender={<h4 />}; - the description as Text with
variant="body-sm"; - then the one Button.
Render it in place of the List or the Table when there is nothing to show, and once something exists, show the page's own action instead.
import { useEffect, useRef, useState } from 'react';
import { Button, Grid, List, Column, Row, Surface, Text } from '@envsemble/envui';
import { IconAdd } from '@envsemble/envui/icons/Add';
import { IconFolderOpen } from '@envsemble/envui/icons/FolderOpen';
export default function FirstUse() {
const [projects, setProjects] = useState<string[]>([]);
const list = useRef<HTMLUListElement>(null);
const leftEmpty = useRef(false);
// The empty state's button goes with it, so focus moves to the list that replaced it.
useEffect(() => {
if (leftEmpty.current) list.current?.focus();
leftEmpty.current = false;
}, [projects]);
const create = () => {
leftEmpty.current = projects.length === 0;
// A real application opens the create form here; the example adds a project at once.
setProjects((current) => [...current, sample[current.length % sample.length]]);
};
return (
<Column gap={4}>
{/* As tall as its button from the start, so the heading stays put as the button comes. */}
<Row
wrap
align="center"
justify="between"
gap={3}
style={{ minBlockSize: 'var(--env-control-height-sm)' }}
>
<Text render={<h3 />} variant="title">
Projects
</Text>
{/* The page's own action appears with the first project; until then the state offers it. */}
{projects.length > 0 && (
<Button size="sm" startIcon={<IconAdd />} onClick={create}>
Create project
</Button>
)}
</Row>
{projects.length > 0 ? (
<List.Root ref={list} aria-label="Projects" tabIndex={-1}>
{projects.map((project, index) => (
<List.Item key={`${project}-${index}`} label={project} />
))}
</List.Root>
) : (
// envUI has no region height for an example: the state fills the place the list will.
<Surface
render={<Grid />}
background="default"
padding={0}
style={{ minBlockSize: '18rem' }}
>
{/* An icon, a heading of the level the page's outline needs, a sentence and the one
action, centred in the room the list will take. */}
<Column
gap={4}
padding={5}
align="center"
justify="center"
style={{ textAlign: 'center' }}
>
<Text render={<span />} emphasis="muted">
<IconFolderOpen width="2.5rem" height="2.5rem" />
</Text>
<Column gap={2} align="center" style={{ maxInlineSize: '30rem' }}>
<Text render={<h4 />} variant="title">
No projects yet
</Text>
<Text variant="body-sm">
A project brings its documents, tasks and people together in one place.
</Text>
</Column>
<Button startIcon={<IconAdd />} onClick={create}>
Create project
</Button>
</Column>
</Surface>
)}
</Column>
);
}
const sample = ['Website refresh', 'Quarterly report', 'Onboarding guide'];Placement and icon size
Centred, the Column takes align="center", justify="center" and textAlign: 'center', with a 2.5 rem icon, in a region that gives it height, such as a Surface with a minBlockSize. At the start, under a toolbar or in a small card, it keeps the default alignment and a 1.5 rem icon.
No results
Keep the SearchField and the Select in the PageHeader's toolbar, and say the count of matches in a Text with role="status", since a neutral state isn't announced. Clear filters resets the query and the filters, then focuses the search, as the button goes with the state.
Load failed
A failure is an Alert with tone="danger" and announce, its Try again Button in actions, in the table's place. Try again moves focus to the region's heading, a Text with tabIndex={-1} and a ref, while the same Table loads as a skeleton over placeholders() rows, as the LoadFailed example does.
<Alert
tone="danger"
announce
title="Couldn’t load documents"
actions={
<Button variant="secondary" onClick={retry}>
Try again
</Button>
}
>
Your documents are safe. Check your connection and try again.
</Alert>
A whole page
Keep the Scaffold and its navigation, and render the state as the router's page: the same Column, centred in the body, with the page's h1 as its heading, as the WholePage example does. Not found offers a Link or Button to the home page, no access a Request access Button, and offline a Try again Button that loads the page again once the connection is back.
<Scaffold topBar={<TopBar title="Northwind" />}>
<Page>
<Column gap={4} padding={5} align="center" justify="center" style={{ textAlign: 'center' }}>
<Text render={<h1 />} variant="title">
Page not found
</Text>
<Button variant="secondary" href="/">
Go to Home
</Button>
</Column>
</Page>
</Scaffold>
Accessibility
You provide
- The heading's level, such as
render={<h4 />}under a section'sh3, so the state takes its place in the page's outline; its look stays the title type whatever the level, except inside a section whose own heading is a title, such as in a pane, where it is body text so it doesn't match its section. - A danger Alert with
announcefor content that failed to load, so it is announced; a neutral state isn't. - The right kind of state, its words and its one action.
- A count of results in a
role="status"text beside a search, since a neutral state isn't announced. - For a whole page, the page's one
h1anddocument.titlesaying what happened, such as "Page not found – Northwind".
envUI provides
- The icons hidden from assistive technology, so the heading and the sentence are what is read.
Changelog
- 1.0.0
Empty states are built from Column, Text and Button, in place of the EmptyState component. The page is on the figure template, with a whole page and the empty states of the content guidelines.