Progressive disclosure

Show what most people need first, and keep the rest one step away, in the place and at the depth that fits it.

BetaSince 1.0.0
On this page

Import

import {
  Accordion,
  Badge,
  Button,
  Checkbox,
  Column,
  DescriptionList,
  Dialog,
  Disclosure,
  IconButton,
  Link,
  List,
  Menu,
  Popover,
  Row,
  Select,
  Sheet,
  Text,
  TextField,
  Tooltip,
} from '@envsemble/envui';

Usage

A Disclosure takes its summary as title and holds the optional settings.

  • Control it with open and onOpenChange where the application opens it, or start it open with defaultOpen.
  • Open it from the application when it holds an invalid field, as the Disclosure page describes.
AdvancedOptions.tsx
import { useEffect, useRef, useState, type FormEvent } from 'react';
import {
  Button,
  Checkbox,
  Disclosure,
  Select,
  Column,
  Row,
  Text,
  TextField,
} from '@envsemble/envui';

export default function AdvancedOptions() {
  const [format, setFormat] = useState<string | null>('csv');
  const [advanced, setAdvanced] = useState(false);
  const [fileName, setFileName] = useState('');
  const [fileNameError, setFileNameError] = useState<string>();
  // The status under the action: the outcome, or the error when an export is refused.
  const [status, setStatus] = useState('');
  const fileNameField = useRef<HTMLInputElement>(null);
  // Counts the refused exports; each focuses the file name once its disclosure has opened.
  const [refused, setRefused] = useState(0);
  useEffect(() => {
    if (refused) fileNameField.current?.focus();
  }, [refused]);

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (fileName && !/^[\w-]+$/.test(fileName)) {
      const message = 'Use only letters, numbers, hyphens and underscores';
      setFileNameError(message);
      // An error inside the disclosure opens it, so the field and its message show.
      setAdvanced(true);
      // Focusing the field reads its error. After Enter in the field, focus is already there and
      // focusing it again reads nothing, so only then does the status say the error.
      if (document.activeElement === fileNameField.current) setStatus(message);
      else {
        setStatus('');
        setRefused((count) => count + 1);
      }
      return;
    }
    setStatus(`Exported ${fileName || 'quarterly-report'}.${format}.`);
  };

  return (
    <form noValidate onSubmit={submit}>
      {/* envUI has no form column yet: one column, kept to a width that reads easily. */}
      <Column gap={5} style={{ maxInlineSize: '32rem' }}>
        <Select
          label="Format"
          name="format"
          items={formats}
          value={format}
          onValueChange={setFormat}
        />
        {/* Settings most people leave alone wait behind a disclosure; nothing required is in it. */}
        <Disclosure title="Advanced options" open={advanced} onOpenChange={setAdvanced}>
          <Column gap={4}>
            <Checkbox name="archived" label="Include archived projects" />
            <TextField
              ref={fileNameField}
              label="File name (optional)"
              name="fileName"
              description="Without one, the file takes the report’s name."
              suffix={`.${format}`}
              autoComplete="off"
              spellCheck={false}
              value={fileName}
              error={fileNameError}
              onValueChange={(value) => {
                setFileName(value);
                if (!value || /^[\w-]+$/.test(value)) {
                  setFileNameError(undefined);
                  setStatus('');
                }
              }}
            />
          </Column>
        </Disclosure>
        <Row wrap gap={3}>
          <Button type="submit">Export report</Button>
        </Row>
        <Text role="status" emphasis="muted">
          {status}
        </Text>
      </Column>
    </form>
  );
}

const formats = [
  { value: 'csv', label: 'CSV' },
  { value: 'pdf', label: 'PDF' },
  { value: 'xlsx', label: 'Spreadsheet (.xlsx)' },
];

Show all

A text Button after the list, with aria-expanded and aria-controls naming the list, toggles between the first few items and all of them, and keeps focus as the list grows.

<Button
  variant="text"
  aria-expanded={expanded}
  aria-controls={listId}
  onClick={() => setExpanded((current) => !current)}
>
  {expanded ? 'Show fewer' : `Show all ${activity.length}`}
</Button>

Details in a sheet

A row's text Button, with aria-haspopup="dialog", opens a Sheet controlled through open and onOpenChange, with the item's details, such as a stacked DescriptionList, and an actions link to its own page. Keep the shown item while the sheet closes, so its content doesn't empty as it slides out.

<Sheet
  open={open}
  onOpenChange={setOpen}
  title={project.name}
  closeLabel="Close details"
  actions={<Button href={`/projects/${project.id}`}>Open project</Button>}
>
  <DescriptionList.Root layout="stacked">{details}</DescriptionList.Root>
</Sheet>

Overflow actions

A Menu named by aria-label takes a ghost IconButton with IconMoreVert and aria-label="More actions" as its trigger, wrapped in a Tooltip that names it. A Menu.Separator sets a destructive item apart, last, as on Menu.

<Menu.Root
  aria-label="Launch brief actions"
  trigger={
    <Tooltip content="More actions">
      <IconButton variant="ghost" icon={<IconMoreVert />} aria-label="More actions" />
    </Tooltip>
  }
>
  <Menu.Item icon={<IconFileCopy />}>Duplicate</Menu.Item>
  <Menu.Separator />
  <Menu.Item icon={<IconArchive />}>Archive</Menu.Item>
</Menu.Root>

Explain a term

A Popover with side="bottom" opens from an IconButton named for the term, such as aria-label="About billable hours", and holds a short definition and a Link.

Composition

The anatomy's parts are the visible layer, here a TextField's env-text-field, the Disclosure's summary, env-disclosure summary, its panel, env-disclosure-panel, and the More actions trigger, env-button ~ env-button.

Accessibility

You provide

  • A name for every icon-only trigger, and aria-expanded and aria-controls on a Show all button.

envUI provides

  • Disclosure on native details and summary, whose closed content is hidden from assistive technology.
  • Focus moved in, trapped while modal and returned by Sheet and Dialog; the menu button pattern for Menu.

Changelog

1.0.0

The page is on the figure template, with Accordion among the depths.