Reticle@etched/reticle 0.0.0

Selectable list

Rows to pick one from, for master–detail screens: conversations, candidate matches, references. Up and Down move focus, so do Home and End, and they skip disabled rows. 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/reticle/badge";
import { SelectableList, type SelectableListItem } from "@etched/reticle/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/reticle/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/reticle/badge";
import { Text } from "@etched/reticle/text";
import { SelectableList, type SelectableListItem } from "@etched/reticle/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/reticle/pill";
import { Text } from "@etched/reticle/text";
import { SelectableList, type SelectableListItem } from "@etched/reticle/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/reticle/pill";
import { SelectableList, type SelectableListItem } from "@etched/reticle/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>
  );
}

API reference

PropTypeDefaultDescription
items *SelectableListItem[]–The rows, in order.
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.
aria-labelstring–Names the list, for example "Conversations".
aria-labelledbystring–The id of visible text that names the list.