Monet
@etched/monet 0.0.1

Selectable list

Ported

Rows to pick one from, for master–detail screens: conversations, candidate matches, references. Up and Down move focus, so do Home, End, Page Up and Page Down (10 rows), and they skip disabled rows. Typing moves focus to the next row whose label starts with the typed letters. Enter, Space or a click selects. Moving focus does not select, so the detail loads only when the user asks. Tab enters at the selected row, or at the first one. Rows hold no interactive content: a screen reader reads each row as text, so a link or button in it cannot be reached.

Ana RuizThanks, Thursday at 2 works for me.2
José NúñezCould you share more about the bring-up team?1
Lee ParkYou: Sent the loop schedule for next week.
import { Badge } from "@etched/monet/badge";
import { SelectableList, type SelectableListItem } from "@etched/monet/selectable-list";

const conversations: SelectableListItem[] = [
  { value: "ana", label: "Ana Ruiz", description: "Thanks, Thursday at 2 works for me.", avatar: { name: "Ana Ruiz" }, end: <Badge>2</Badge> },
  { value: "jose", label: "José Núñez", description: "Could you share more about the bring-up team?", avatar: { name: "José Núñez" }, end: <Badge>1</Badge> },
  { value: "lee", label: "Lee Park", description: "You: Sent the loop schedule for next week.", avatar: { name: "Lee Park" } },
  { value: "mira", label: "Mira Osei", description: "Not looking right now. Maybe after the tape-out.", avatar: { name: "Mira Osei" } },
  { value: "kai", label: "Kai Ito", description: "Happy to chat. Is the role in Cupertino?", avatar: { name: "Kai Ito" } },
];

export function Preview() {
  return (
    <div className="card" style={{ inlineSize: "18rem" }}>
      <SelectableList aria-label="Conversations" items={conversations.slice(0, 3)} defaultValue="jose" />
    </div>
  );
}

Installation

import { SelectableList } from "@etched/monet/selectable-list";

Examples

Conversations

Up and Down move focus; Enter, Space or a click opens the conversation. Moving focus does not open it.

Ana RuizThanks, Thursday at 2 works for me.2
José NúñezCould you share more about the bring-up team?1
Lee ParkYou: Sent the loop schedule for next week.
Mira OseiNot looking right now. Maybe after the tape-out.
Kai ItoHappy to chat. Is the role in Cupertino?

Showing the conversation with Lee Park.

import { useState } from "react";
import { Badge } from "@etched/monet/badge";
import { Text } from "@etched/monet/text";
import { SelectableList, type SelectableListItem } from "@etched/monet/selectable-list";

const conversations: SelectableListItem[] = [
  { value: "ana", label: "Ana Ruiz", description: "Thanks, Thursday at 2 works for me.", avatar: { name: "Ana Ruiz" }, end: <Badge>2</Badge> },
  { value: "jose", label: "José Núñez", description: "Could you share more about the bring-up team?", avatar: { name: "José Núñez" }, end: <Badge>1</Badge> },
  { value: "lee", label: "Lee Park", description: "You: Sent the loop schedule for next week.", avatar: { name: "Lee Park" } },
  { value: "mira", label: "Mira Osei", description: "Not looking right now. Maybe after the tape-out.", avatar: { name: "Mira Osei" } },
  { value: "kai", label: "Kai Ito", description: "Happy to chat. Is the role in Cupertino?", avatar: { name: "Kai Ito" } },
];

export function Conversations() {
  const [open, setOpen] = useState<string | null>("lee");
  const person = conversations.find((c) => c.value === open)?.label;

  return (
    <div className="grid">
      <div className="card">
        <SelectableList aria-label="Conversations" items={conversations} value={open} onValueChange={setOpen} />
      </div>
      <Text variant="secondary">Showing the conversation with {person}.</Text>
    </div>
  );
}

Candidate matches

Nothing is selected yet, so Tab enters at the first row. Visible text names the list through aria-labelledby.

Is this one of your candidates?

Ana RuizStaff engineer at Tenstorrent94% match
Ana Ruíz OrtegaDesign verification at Cerebras71% match
A. RuizReferral from Sam Lee, 202438% match
import { Pill } from "@etched/monet/pill";
import { Text } from "@etched/monet/text";
import { SelectableList, type SelectableListItem } from "@etched/monet/selectable-list";

const matches: SelectableListItem[] = [
  { value: "c-1042", label: "Ana Ruiz", description: "Staff engineer at Tenstorrent", avatar: { name: "Ana Ruiz" }, end: <Pill tone="green">94% match</Pill> },
  { value: "c-2210", label: "Ana Ruíz Ortega", description: "Design verification at Cerebras", avatar: { name: "Ana Ruíz Ortega" }, end: <Pill tone="amber">71% match</Pill> },
  { value: "c-0388", label: "A. Ruiz", description: "Referral from Sam Lee, 2024", avatar: { name: "A. Ruiz" }, end: <Pill>38% match</Pill> },
];

export function CandidateMatches() {
  return (
    <div className="grid">
      <div className="card">
        <Text id="matches-label" bold DANGEROUS_style={{ padding: "var(--et-space-2) var(--et-space-4) 0" }}>
          Is this one of your candidates?
        </Text>
        <SelectableList aria-labelledby="matches-label" items={matches} />
      </div>
    </div>
  );
}

Disabled rows

A disabled row cannot be selected, and the arrow keys skip it. Say why in its description.

Sam LeeManager at Groq, 2021–2024Received
Priya ShahNo email or phone yetTo do
Kai ItoPeer at Cerebras, 2019–2021Scheduled
Tom WeberReport at SambaNova, 2022–2024Activating
import { Pill } from "@etched/monet/pill";
import { SelectableList, type SelectableListItem } from "@etched/monet/selectable-list";

const references: SelectableListItem[] = [
  { value: "sam", label: "Sam Lee", description: "Manager at Groq, 2021–2024", end: <Pill tone="green">Received</Pill> },
  { value: "priya", label: "Priya Shah", description: "No email or phone yet", end: <Pill>To do</Pill>, disabled: true },
  { value: "kai", label: "Kai Ito", description: "Peer at Cerebras, 2019–2021", end: <Pill tone="purple">Scheduled</Pill> },
  { value: "tom", label: "Tom Weber", description: "Report at SambaNova, 2022–2024", end: <Pill tone="blue">Activating</Pill> },
];

export function DisabledRows() {
  return (
    <div className="grid">
      <div className="card">
        <SelectableList aria-label="References" items={references} defaultValue="sam" />
      </div>
    </div>
  );
}

Groups and type-to-find

Groups put rows under headings. Type a name's first letters to move to it; the same letter again moves on to the next match. Page Up and Page Down move 10 rows.

Today
Ana RuizASIC design engineer
Kai ItoFirmware engineer
Mira OseiPhysical design lead
This week
Maya ChenThermal engineer
Sam LeeRecruiter
José NúñezVerification engineer
Priya ShahPackaging engineer
Earlier
Lee ParkTest engineer
Marco RossiBoard designer
Tom WeberCompiler engineer
Aiko TanakaPower engineer
Noah KimSystems engineer
Rosa DíazDesign verification
Ben OkaforProduct manager
import { SelectableList, type SelectableListGroup, type SelectableListItem } from "@etched/monet/selectable-list";

const sender = (name: string, description: string): SelectableListItem => ({ value: name, label: name, description, avatar: { name } });

const senders: SelectableListGroup[] = [
  { label: "Today", items: [sender("Ana Ruiz", "ASIC design engineer"), sender("Kai Ito", "Firmware engineer"), sender("Mira Osei", "Physical design lead")] },
  {
    label: "This week",
    items: [sender("Maya Chen", "Thermal engineer"), sender("Sam Lee", "Recruiter"), sender("José Núñez", "Verification engineer"), sender("Priya Shah", "Packaging engineer")],
  },
  {
    label: "Earlier",
    items: [
      sender("Lee Park", "Test engineer"),
      sender("Marco Rossi", "Board designer"),
      sender("Tom Weber", "Compiler engineer"),
      sender("Aiko Tanaka", "Power engineer"),
      sender("Noah Kim", "Systems engineer"),
      sender("Rosa Díaz", "Design verification"),
      sender("Ben Okafor", "Product manager"),
    ],
  },
];

export function GroupsAndTypeToFind() {
  return (
    <div className="grid">
      <div className="card" style={{ maxBlockSize: "22rem", overflowY: "auto" }}>
        <SelectableList aria-label="Senders" items={senders} />
      </div>
    </div>
  );
}

Empty and loading

loading shows placeholder rows and marks the list busy. With no rows, empty says why, and the list takes focus itself so keyboard and screen reader users hear it.

import { SelectableList } from "@etched/monet/selectable-list";

export function EmptyAndLoading() {
  return (
    <div className="grid">
      <div className="card">
        <SelectableList aria-label="Candidate matches" items={[]} loading />
      </div>
      <div className="card">
        <SelectableList aria-label="Archived conversations" items={[]} empty="No archived conversations." />
      </div>
    </div>
  );
}

Accessibility

  • ArrowUp/ArrowDown, Home/End and PageUp/PageDown (10 rows) move focus and skip disabled rows. Typing moves to a row whose label starts with the letters.
  • Enter, Space or a click selects; moving focus does not. Tab enters at the selected row, or at the first row.
  • A listbox of option rows, named by the required aria-label or aria-labelledby. A row's label names it; its description and end describe it.
  • The selected row has aria-selected="true", and groups are named by their heading. While loading the list is aria-busy; an empty list is described by empty.
  • Put no links or buttons in a row: screen readers read each row as text. Load the detail in onValueChange.

API reference

PropTypeDefaultDescription
items *(SelectableListItem | SelectableListGroup)[]–The rows in order, or groups of rows under headings.
valuestring | null–The selected row, controlled. null selects none.
defaultValuestring | nullnullThe first selected row, uncontrolled.
onValueChange(value: string) => void–Called when Enter, Space or a click selects a row. Moving focus does not select.
emptyReactNode"No items."Shown when there are no rows, such as "No conversations yet". The list then takes focus itself and screen readers hear this text.
loadingboolean–Placeholder rows in place of the rows, while they load. The list is aria-busy.
aria-labelstring–Names the list, for example "Conversations".
aria-labelledbystring–The id of visible text that names the list.