Skip to content

EmptyState ​

An empty state is what a collection shows when it has nothing to show. It is one element doing one job — muted text in the place the rows would have been — and it exists so that a ListBox, a Menu or a Table can hand you the slot and leave the sentence to you.

ts
import { EmptyState } from "ropav";

In a collection's empty slot ​

ListBox, Menu, DropdownMenu and TableBody each take an #empty slot, which renders in place of the rows while the collection has nothing in it. Nothing has to be switched on: the slot is used when there is nothing else to use it for.

Nobody matches that search.
Ada Lovelace
Grace Hopper
Katherine Johnson
empty-state-collection.vue
vue
<script setup lang="ts">
import {
  EmptyState,
  Label,
  ListBox,
  ListBoxItem,
  SearchField,
  SearchFieldClearButton,
  SearchFieldGroup,
  SearchFieldInput,
  SearchFieldSearchIcon,
  Surface,
} from "ropav";
import { computed, shallowRef } from "vue";

const people = [
  { id: "ada", name: "Ada Lovelace" },
  { id: "grace", name: "Grace Hopper" },
  { id: "katherine", name: "Katherine Johnson" },
];

const query = shallowRef("");

const matched = computed(() =>
  people.filter((person) => person.name.toLowerCase().includes(query.value.toLowerCase())),
);
</script>

<template>
  <div class="stack">
    <SearchField v-model:value="query" aria-label="Search reviewers">
      <SearchFieldGroup>
        <SearchFieldSearchIcon />
        <SearchFieldInput placeholder="Search reviewers…" />
        <SearchFieldClearButton />
      </SearchFieldGroup>
    </SearchField>

    <Surface class="surface">
      <ListBox aria-label="Reviewers">
        <template #empty>
          <EmptyState>Nobody matches that search.</EmptyState>
        </template>

        <ListBoxItem v-for="person in matched" :id="person.id" :key="person.id">
          <Label>{{ person.name }}</Label>
        </ListBoxItem>
      </ListBox>
    </Surface>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  width: calc(var(--rp-spacing) * 64);
  flex-direction: column;
  gap: calc(var(--rp-spacing) * 3);
}

.surface {
  padding: 0;
}
</style>

A message that names the reason is worth more than one that names the state. "Nobody matches that search" tells the reader their query was the problem; "No results" leaves them to work out whether anything was ever there.

The default is a fallback ​

<EmptyState /> with nothing inside reads "No results found". That default is slot fallback content rather than a check on what was handed in, because inspecting a slot means executing it, and executing it is what creates the DOM.

A popover has to be allowed to open empty ​

Select and ComboBox keep their popover shut when there are no options, which means the empty state inside it never gets the chance to say so. allows-empty-collection is what lets the popover open on nothing — without it, a search that matches no option looks like a control that has stopped responding.

Accessibility ​

  • Inside a list box the message is rendered as a disabled option rather than as loose text, so a reader arrowing through the list meets it where the options would have been instead of finding silence.
  • It carries no live region. An empty state is content that replaced other content, not an event to announce — the collection it sits in is what a reader is already listening to.
  • The text is the whole component, so it has to read as a sentence on its own. It is reached without the surrounding heading, which is what makes an icon or a bare "None" too little.

API ​

Slots: default.