> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ada.cx/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ada.cx/_mcp/server.

# Components

This page catalogs every component that `@ada-cx/lovelace` exports, grouped by role. Each entry names the component's purpose and its most important props, and pairs them with a copyable example. Prop types ship with the package as TypeScript declarations, so your editor completes and checks every prop.

Each entry opens with a live preview of the component. The previews use the theme and color scheme you set in the [theme playground](/lovelace/theming#theme-playground).

## Shared conventions

All components follow the same contract:

* Check each component's TypeScript declarations for its supported `className`, `ref`, and `style` props.
* Interactive components extend the matching [React Aria Components](https://react-spectrum.adobe.com/react-aria/) props. Use `onPress` instead of `onClick`, and `isDisabled` instead of `disabled`.
* Interactive states render as `data-*` attributes: `data-disabled`, `data-focus-visible`, `data-hovered`, `data-pressed`, `data-selected`. Extend styling by targeting those attributes, not pseudo-classes.
* Compound components attach their parts with dot notation, for example `Input.Field` and `Dialog.Title`. The parts are not separate exports.
* `IconButton` and `Input.Button` require a `label` prop for the accessible name. `Chip` with `variant="icon"` and an icon-only `PillButton` take `aria-label` instead.

## Conversation

Components that render the conversation itself. The chat composer is not a separate export: it is the `Input` component's `"composer"` variant, with a send `Input.Button`. See [Input](#input).

### AgentMessage

A single agent message body. The content accepts blocks, such as lists and tables. Key props: `children`.

Row layout is yours, not the component's. Place an avatar, a sender name or a retry control beside the body rather than inside it, so they align with the text and stay outside the body's own text-direction detection. [MessageRow](#messagerow) supplies that layout. Use `<Avatar showAvatar={false}>` to hold the gutter on a grouped message that shows no avatar.

```tsx
import { AgentMessage, Avatar } from "@ada-cx/lovelace";

<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
  <div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
    <Avatar aria-hidden>A</Avatar>
    <AgentMessage>Hi! I can help with orders, billing, and returns.</AgentMessage>
  </div>
  <div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>
    <Avatar aria-hidden showAvatar={false} />
    <AgentMessage>Which order do you need help with?</AgentMessage>
  </div>
</div>;
```

### Divider

A transcript separator: a plain rule, a labeled marker (for example "1 unread message"), or a centered system message. Key props: `children`, `heading`, `variant` (`"label"` | `"line"` | `"message"`).

```tsx
import { Divider } from "@ada-cx/lovelace";

<>
  <Divider variant="line" />
  <Divider variant="label">1 unread message</Divider>
  <Divider variant="message" heading="⏳ You are #1 in the queue">
    An agent will be with you shortly.
  </Divider>
</>;
```

### DropZone

A region that accepts dropped files. While a drag is over the region, a panel covers its children. Key props: `allowMultiple`, `description`, `ignorePageDrags`, `isDisabled`, `label`, `onDropError`, `onFileDrop`, `onFilesDrop`, `onMultipleFilesRejected`, `onNoFilesDropped`.

Supply exactly one of `onFileDrop` and `onFilesDrop`. `onFileDrop` receives one file per drop: a drop with more files calls `onMultipleFilesRejected`, unless `allowMultiple` is set. `onFilesDrop` receives every readable file of a drop in one call. `label` is the panel heading and the accessible name of the keyboard drop button.

The panel fills the nearest positioned ancestor, so that ancestor sets the droppable area. Set `ignorePageDrags` when the zone covers content that the user can drag, such as text or images in the transcript.

```tsx
import { DropZone } from "@ada-cx/lovelace";

<div style={{ position: "relative" }}>
  <DropZone
    label="Drop your file here"
    description="One file at a time"
    onFileDrop={stageFile}
    ignorePageDrags
  >
    <Transcript />
  </DropZone>
</div>;
```

### Header

The conversation header bar arranges its children in one row. Key props: `children`.

Parts:

* `Header.Title` renders the header heading. Key props: `children`, `level`.

```tsx
import { Avatar, Header } from "@ada-cx/lovelace";

<Header>
  <Avatar size="md" label="Ada AI Agent">
    A
  </Avatar>
  <Header.Title>Ada</Header.Title>
</Header>;
```

### MessageError

The failed-to-send row shown below a message bubble. With `onRetry` the copy becomes a retry control and names it. Without `onRetry` the copy renders as static text. Key props: `announce`, `children`, `onRetry`.

`children` must be a plain string. It supplies the announced text and the retry control's name.

The row announces the failure through a visually-hidden `role="alert"`. Set `announce` to `false` on a surface that announces message failures through its own live region. Two live regions announce the same text twice.

Without `onRetry`, the row hides the visible copy from assistive technology while `announce` stays on. The alert already carries that text. With `onRetry`, the copy stays exposed, because it names the retry control.

The row also aligns at the leading edge of its container. Put it in an end-aligned column to keep it below the bubble.

```tsx
import { MessageError, UserMessage } from "@ada-cx/lovelace";

<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end" }}>
  <UserMessage>This one failed to send.</UserMessage>
  <MessageError onRetry={resend}>Message failed to send. Retry</MessageError>
</div>;
```

### MessageRow

One row of a transcript: an optional avatar gutter, then `MessageRow.Content`, the column that holds everything belonging to the message. Key props: `sender`. Required. Accepts `"agent"` or `"user"`.

`sender` sets which edge the row aligns to and the typography the column carries. The row imposes no message body. A row that shows prose renders `AgentMessage` or `UserMessage` inside the column. A row that shows a card, a widget or media renders neither, and still reads at message size. Set `fill` when the content supplies no width of its own.

Keep the avatar a sibling of the column rather than a child of it. A reaction bar or a retry control beside the message then aligns with the message text.

Parts:

* `MessageRow.Content` holds the message body and related controls. Key props: `children`, `fill`.

```tsx
import { AgentMessage, Avatar, MessageRow } from "@ada-cx/lovelace";

<MessageRow sender="agent">
  <Avatar aria-hidden size="sm">A</Avatar>
  <MessageRow.Content>
    <AgentMessage>Hi! I can help with orders, billing, and returns.</AgentMessage>
  </MessageRow.Content>
</MessageRow>;
```

### PictureMessage

An image message frame with a built-in "image unavailable" placeholder. Key props: `alt`, `aspectRatio` (`"1:1"` | `"16:9"` | `"3:4"` | `"4:3"` | `"9:16"`), `onLoadingStatusChange`, `src`, `unavailable`.

```tsx
import { PictureMessage } from "@ada-cx/lovelace";

<PictureMessage
  src="https://example.com/photo.jpg"
  alt="Order photo"
  aspectRatio="16:9"
/>;
```

### ProactiveMessage

A proactive greeting card shown outside the conversation window. Key props: `children`.

```tsx
import { ProactiveMessage } from "@ada-cx/lovelace";

<ProactiveMessage>
  👋 Need a hand picking a plan? I can compare them for you.
</ProactiveMessage>;
```

### ScrollMarker

A floating jump-to-bottom control; shows an unread pill when messages wait below the fold. Key props: `children`, `color` (`"brand"` | `"default"`), `onPress`, `unreadCount`.

```tsx
import { ScrollMarker } from "@ada-cx/lovelace";

<ScrollMarker unreadCount={2} onPress={scrollToBottom} />;
```

### UserMessage

The end user's outbound message bubble, shrink-wrapped to its content. The content accepts blocks, such as lists and tables. Key props: `children`.

Where the bubble sits is yours: align it to the trailing edge in your own row. Render `MessageError` beside the bubble when a send fails.

```tsx
import { UserMessage } from "@ada-cx/lovelace";

<UserMessage>Where is my order?</UserMessage>;
```

### VideoMessage

The frame for a video message. It clips the player to the media corner radius, keeps a fixed aspect ratio, and gives the video an accessible group label. The frame has no playback logic: supply your own player as `children`. Key props: `alt` (required), `aspectRatio` (`"1:1"` | `"16:9"` | `"3:4"` | `"4:3"` | `"9:16"`), `children`, `unavailable`.

The frame shows a "video unavailable" placeholder when `unavailable` is set or when `children` renders nothing. The frame fills the width of its container, so constrain it to the message column.

```tsx
import { VideoMessage } from "@ada-cx/lovelace";

<VideoMessage alt="Unboxing the replacement part" aspectRatio="16:9">
  <video
    src="https://example.com/unboxing.mp4"
    controls
    playsInline
    style={{ width: "100%", height: "100%", objectFit: "cover" }}
  />
</VideoMessage>;
```

## Actions

Buttons and links that trigger an action.

### Button

The standard action button. Key props: `destructive`, `isDisabled`, `onPress`, `size` (`"md"` | `"sm"`), `variant` (`"primary"` | `"secondary"` | `"tertiary"`).

```tsx
import { Button } from "@ada-cx/lovelace";

<>
  <Button variant="primary" onPress={submit}>
    Primary
  </Button>
  <Button variant="secondary">Secondary</Button>
  <Button variant="tertiary">Tertiary</Button>
  <Button variant="primary" destructive>
    Delete
  </Button>
</>;
```

### IconButton

An icon-only button. Key props: `children`, `destructive`, `label`, `size` (`"md"` | `"sm"` | `"xs"`), `variant` (`"primary"` | `"secondary"` | `"tertiary"`). Supply one icon as `children` and an accessible name as `label`.

```tsx
import { IconButton, Send } from "@ada-cx/lovelace";

<IconButton label="Send message" onPress={send}>
  <Send />
</IconButton>;
```

### Link

A standalone link control with a trailing external-link icon by default. With `download`, it shows a download icon. Set `showIcon={false}` to hide the icon. Its box cannot split across surrounding prose lines. Text inside the control can wrap. Key props: `download`, `href`, `showIcon`, `underlined`.

```tsx
import { Link } from "@ada-cx/lovelace";

<Link href="https://docs.ada.cx" underlined showIcon>
  Read the developer docs
</Link>;
```

### PillButton

A pill-shaped secondary action button. Key props: `isDisabled`, `onPress`, `size` (`"md"` | `"sm"`).

```tsx
import { PillButton } from "@ada-cx/lovelace";

<PillButton onPress={endChat}>End chat</PillButton>;
```

## Forms and selection

Form and selection controls, including the chat composer.

### AttachmentChip

A staged or attached file inside the composer field: a thumbnail or a stand-in, the file name, and one control. Key props: `action`, `aria-expanded`, `aria-haspopup`, `isBusy`, `label`, `media`, `onImageError`, `onPress`, `thumbnailUrl`.

With `onPress`, set `aria-haspopup="dialog"` when the chip opens a sheet. Set `aria-expanded` to `true` while the sheet is open.

The chip has two shapes. With `action`, the chip is a container and the trailing control, typically a remove `IconButton`, is its only control. With `onPress`, the chip itself is the control, for example a "3 attachments" summary that opens a sheet. The chip ignores `action` when `onPress` is set. Place chips in `Input.Attachments`: see [Input](#input).

```tsx
import { AttachmentChip, IconButton, Paperclip, Xmark } from "@ada-cx/lovelace";

<AttachmentChip
  label="receipt.png"
  media={<Paperclip />}
  action={
    <IconButton label="Remove receipt.png" variant="tertiary" size="xs" onPress={removeFile}>
      <Xmark />
    </IconButton>
  }
/>;
```

### Checkbox

A check box with an optional inline label. Key props: `children`, `isSelected`, `onChange`, `variant`.

`variant` accepts `"inline"` or `"row"`. The default, `inline`, puts the control beside its label. `row` renders a full-width option row with a **minimum height of 40px**, which grows for wrapped labels.

```tsx
import { Checkbox } from "@ada-cx/lovelace";

<Checkbox defaultSelected>Email me a transcript</Checkbox>;
```

### CheckboxGroup

`CheckboxGroup` groups `Checkbox` options and owns the selected values. Key props: `aria-label`, `children`, `onChange`, `value`.

```tsx
import { Checkbox, CheckboxGroup } from "@ada-cx/lovelace";

<CheckboxGroup aria-label="Topics to follow up on" defaultValue={["billing"]}>
  <Checkbox value="billing">Billing</Checkbox>
  <Checkbox value="delivery">Delivery</Checkbox>
</CheckboxGroup>;
```

### Chip

A toggle chip for quick replies and filters. Key props: `children`, `isSelected`, `onChange`, `size` (`"lg"` | `"md"`), `variant` (`"icon"` | `"number"` | `"text"`).

The `icon` variant has no visible text. Pass `aria-label`.

```tsx
import { Chip } from "@ada-cx/lovelace";

<>
  <Chip defaultSelected>Track my order</Chip>
  <Chip>Talk to an agent</Chip>
</>;
```

### Input

`Input` coordinates the text control, label, and helper text. Key props: `isDisabled`, `isInvalid`, `onChange`, `value`, `variant` (`"composer"` | `"default"`).

Parts:

* `Input.Attachments` holds the attachment chips. Key props: `children`.
* `Input.Button` adds an icon button to the field. Key props: `children`, `isDisabled`, `label`, `onPress`.
* `Input.Control` renders a single line of text. Key props: `placeholder`, `type`.
* `Input.Field` groups the control, attachments, and button. Key props: `children`, `hasAttachments`.
* `Input.HelperText` describes the input. Key props: `children`.
* `Input.Label` supplies the input's visible label. Key props: `children`.
* `Input.TextArea` grows with its content. Key props: `maxRows`, `minRows`, `placeholder`.

```tsx
import { Input } from "@ada-cx/lovelace";

<Input aria-label="Email address">
  <Input.Label>Email address</Input.Label>
  <Input.Field>
    <Input.Control placeholder="you@example.com" />
  </Input.Field>
  <Input.HelperText>We reply within a day.</Input.HelperText>
</Input>;
```

The composer variant adds the send button and an auto-growing text area:

```tsx
import { Input, Send } from "@ada-cx/lovelace";

<Input variant="composer" aria-label="Message">
  <Input.Field>
    <Input.TextArea placeholder="Write a message" minRows={1} maxRows={3} />
    <Input.Button label="Send">
      <Send />
    </Input.Button>
  </Input.Field>
</Input>;
```

`Input.Attachments` holds attachment chips after the control and before `Input.Button`. Set `hasAttachments` on `Input.Field`. The control fills the first line. The chips and send button share the second line, aligned at the bottom. This replaces the attachment-first layout used before version 1.12.0.

```tsx
import { AttachmentChip, Document, Input, Send } from "@ada-cx/lovelace";

<Input variant="composer" aria-label="Message with attachment">
  <Input.Field hasAttachments>
    <Input.TextArea placeholder="Add a caption" minRows={1} maxRows={3} />
    <Input.Attachments>
      <AttachmentChip label="1 attachment" media={<Document />} onPress={openAttachments} />
    </Input.Attachments>
    <Input.Button label="Send" onPress={send}>
      <Send />
    </Input.Button>
  </Input.Field>
</Input>;
```

`Input.Field` allows its children to wrap in both variants. A child that cannot shrink moves to a new line.

### Radio and RadioGroup

`RadioGroup` groups `Radio` options with keyboard navigation. Key props for `Radio`: `children`, `value`, `variant`. Key props for `RadioGroup`: `aria-label`, `children`, `onChange`, `value`.

`Radio.variant` accepts `"inline"` or `"row"`. The default, `inline`, puts the control beside its label. `row` renders a full-width option row with a **minimum height of 40px**, which grows for wrapped labels.

```tsx
import { Radio, RadioGroup } from "@ada-cx/lovelace";

<RadioGroup aria-label="Contact method" defaultValue="chat">
  <Radio value="chat">Continue in chat</Radio>
  <Radio value="email">Switch to email</Radio>
</RadioGroup>;
```

### SurveyRating

A numeric or icon rating scale (for example CSAT 1 to 5). Provide `aria-label`, or use `aria-labelledby` to reference the visible question label. Key props: `aria-label`, `aria-labelledby`, `defaultSelectedKey`, `isDisabled`, `onChange`, `options`, `selectedKey`, `showLabels`.

Each option accepts these fields:

```typescript
{
  "aria-label"?: string;
  icon?: ReactNode;
  key?: Key;
  label?: string;
  value: number;
}
```

Set `icon` to render an icon chip instead of the number. Give each icon option a `label` or `aria-label` for its accessible name.
`key` defaults to the option's index. Use `defaultSelectedKey` for an uncontrolled scale.
`onChange(key, option)` receives `null` for both arguments on deselection.

```tsx
import { SurveyRating } from "@ada-cx/lovelace";

<SurveyRating
  aria-label="How satisfied are you?"
  showLabels
  options={[
    { value: 1, label: "Not satisfied" },
    { value: 2 },
    { value: 3 },
    { value: 4 },
    { value: 5, label: "Very satisfied" },
  ]}
/>;
```

### SurveySelect

A chip group for single or multiple choice survey questions. Provide `aria-label`, or use `aria-labelledby` to reference the visible question label. Key props: `aria-label`, `aria-labelledby`, `disallowEmptySelection`, `onChange`, `options`, `selectedKeys`, `selectionMode` (`"multiple"` | `"single"`).

```tsx
import { SurveySelect } from "@ada-cx/lovelace";

<SurveySelect
  aria-label="What did we help with?"
  selectionMode="multiple"
  options={[{ label: "Orders" }, { label: "Billing" }, { label: "Returns" }]}
/>;
```

### Toggle

An on/off switch. Track-only: name it with `aria-label`. Key props: `aria-label`, `isSelected`, `onChange`.

```tsx
import { Toggle } from "@ada-cx/lovelace";

<Toggle defaultSelected aria-label="Sound on" />;
```

## Feedback and status

Components that report what the system is doing.

### Banner

The tinted full-width strip that reports a condition, such as an outage, a lost connection, or a failed send. It carries no control, because only the state that raised the condition takes the banner down. It is presentational and owns no live region, so it announces nothing on its own. Put it in a `BannerContainer`. Key props: `children`, `status` (`"error"` | `"success"` | `"warning"`).

```tsx
import { Banner } from "@ada-cx/lovelace";

<Banner status="error">Your network appears to be offline.</Banner>;
```

### BannerContainer

Announces and animates one `Banner`. It owns the live region and mounts it empty, so a banner that arrives is a change the screen reader reports. `error` interrupts as an assertive `alert`. Any other status waits its turn as a polite `status`. The container clips the banner's travel, so you need no `overflow` rule of your own. Mount it for as long as the surface exists. It never queues and never times out, and it holds one banner at a time, so pick the condition to show before you hand it over. Key props: `children`, `occurrence`.

Change `occurrence` whenever the reader must hear the banner again: the same condition raised a second time with the same wording, a different condition taking the slot, or a change of status. A change hides the live region for a frame and reveals it, which is what makes wording the reader has already heard read out again. Leave it alone to reword the banner, and the live region reports the new text on its own.

```tsx
import { Banner, BannerContainer } from "@ada-cx/lovelace";

<BannerContainer occurrence={`offline:${attempt}`}>
  {isOffline ? (
    <Banner status="error">Your network appears to be offline.</Banner>
  ) : null}
</BannerContainer>;
```

### Spinner

A loading spinner for inline or region loading states. Key props: `background` (`"accent"` | `"default"`), `label`, `size` (`"lg"` | `"sm"`).

```tsx
import { Spinner } from "@ada-cx/lovelace";

<Spinner size="lg" label="Loading" />;
```

### ThinkingShimmer

An animated shimmer label shown while the AI Agent generates a reply. Key props: `label`.

```tsx
import { ThinkingShimmer } from "@ada-cx/lovelace";

<ThinkingShimmer label="Thinking" />;
```

### Toast

The floating card that `ToastContainer` renders. It sits above the transcript on a neutral background, so a passing event does not look like the tinted strip a lasting condition uses. It carries no live region of its own, so it announces nothing when you render it alone. Use it directly only inside a container that owns the announcement. Key props: `children`, `dismissDisabled`, `dismissLabel`, `icon`, `messageProps`, `onDismiss`, `status` (`"custom"` | `"error"` | `"loading"` | `"success"` | `"warning"`).

Set `icon` to supply the glyph for `status="custom"`. The `loading` status shows a spinner and renders no dismiss button.

```tsx
import { Toast } from "@ada-cx/lovelace";

<Toast status="success" onDismiss={() => setShown(false)}>
  Transcript sent
</Toast>;
```

### ToastContainer

Shows the toasts on a queue as a labelled landmark region. The container owns announcement, the card surface, and the motion. It names the dismiss button in the reader's own language. It keeps the region reachable with F6. The container shows no toast while a modal overlay is open, and shows the queue again after the overlay closes. Create one queue for each surface.

Lovelace overlays register themselves. For your own React Aria `ModalOverlay`, call `useRegisterOverlay()` from a component inside it. Then `ToastContainer` and `useHasOpenOverlay` detect the open overlay.

You own the clock. Decide which toasts exist, how long each one lives, and what a dismiss press means. Put a key in `exiting` to start that card's travel off the stack, and keep the card on the queue until `onExited` reports it. Key props: `aria-label`, `exiting`, `onDismiss`, `onExited`, `queue`.

Hold your countdowns for two cases the reader cannot read through. A modal overlay hides the stack, so a countdown that runs on expires a card the reader never saw. The `useHasOpenOverlay` hook reports whether one is open. Keyboard focus inside the stack is the reader asking for more time, which is the only extension a card that dismisses itself offers. Listen for `focusin` and `focusout` on the container, and hold for as long as focus stays. The example below holds for the overlay; hold for focus the same way.

Each queue entry takes `{ message, status?, icon? }`. The `status` accepts `"custom"`, `"error"`, `"loading"`, `"success"`, and `"warning"`, and defaults to `"success"`. Set `icon` to supply the glyph for `status: "custom"`. A `loading` toast renders no dismiss button, so close or replace it from the code that queued it.

```tsx
import { ToastContainer, ToastQueue, useHasOpenOverlay } from "@ada-cx/lovelace";
import type { ToastItem } from "@ada-cx/lovelace";
import { useCallback, useEffect, useRef, useState } from "react";

type Timer = { id: number | null; remainingMs: number; startedAt: number };

const toasts = new ToastQueue<ToastItem>();
const TIMEOUT_MS = 5000;

function Toasts() {
  const [exiting, setExiting] = useState<ReadonlySet<string>>(new Set());
  const timers = useRef(new Map<string, Timer>());
  const hasOpenOverlay = useHasOpenOverlay();

  const startExit = useCallback((key: string) => {
    window.clearTimeout(timers.current.get(key)?.id ?? undefined);
    timers.current.delete(key);
    setExiting((current) => new Set(current).add(key));
  }, []);

  const finishExit = useCallback((key: string) => {
    setExiting((current) => {
      const next = new Set(current);
      next.delete(key);
      return next;
    });
    toasts.close(key);
  }, []);

  const arm = useCallback(
    (key: string) => {
      const timer = timers.current.get(key);
      if (!timer) return;
      timer.startedAt = Date.now();
      timer.id = window.setTimeout(() => startExit(key), timer.remainingMs);
    },
    [startExit],
  );

  useEffect(() => {
    for (const [key, timer] of timers.current) {
      if (hasOpenOverlay && timer.id !== null) {
        window.clearTimeout(timer.id);
        timer.id = null;
        timer.remainingMs -= Date.now() - timer.startedAt;
      } else if (!hasOpenOverlay && timer.id === null) {
        arm(key);
      }
    }
  }, [hasOpenOverlay, arm]);

  const send = () => {
    const key = toasts.add({ message: "Transcript sent", status: "success" });
    timers.current.set(key, { id: null, remainingMs: TIMEOUT_MS, startedAt: 0 });
    if (!hasOpenOverlay) {
      arm(key);
    }
  };

  return (
    <>
      <button onClick={send}>Send transcript</button>
      <ToastContainer
        queue={toasts}
        exiting={exiting}
        onExited={finishExit}
        onDismiss={startExit}
      />
    </>
  );
}
```

### TypingIndicator

An animated three-dot bubble shown while a human agent types. Use `ThinkingShimmer` for AI generation instead. Key props: `aria-label`, `aria-live`.

```tsx
import { TypingIndicator } from "@ada-cx/lovelace";

<TypingIndicator aria-label="Agent is typing" />;
```

## Overlays and menus

Modal and floating surfaces. The overlay components own presentation (scrim, focus trap, dismissal); the content components own layout.

### BubbleOverlay

A non-dimming modal bubble near the bottom edge; the chat behind it stays visible. Key props: `aria-label`, `isDismissable`, `isOpen`, `onOpenChange`.

```tsx
import { BubbleOverlay } from "@ada-cx/lovelace";
import { useState } from "react";

function QuickHelp() {
  const [open, setOpen] = useState(false);
  return (
    <BubbleOverlay
      isOpen={open}
      onOpenChange={setOpen}
      isDismissable
      aria-label="Quick help"
    >
      <p>A non-dimming bubble near the bottom edge.</p>
    </BubbleOverlay>
  );
}
```

### Dialog

Dialog content groups an icon, title, body, and action buttons. Key props: `children`, `className`. Wrap the content in `DialogOverlay` for modal behavior.

Parts:

* `Dialog.Actions` stacks the action buttons. Key props: `children`, `className`.
* `Dialog.Body` renders the dialog description. Key props: `children`, `className`, `id`.
* `Dialog.Content` groups the title and description. Key props: `children`, `className`.
* `Dialog.Icon` shows the status icon. Key props: `className`, `intent` (`"success"` | `"warning"`).
* `Dialog.Title` supplies the dialog's accessible name. Key props: `children`, `className`.

```tsx
import { Button, Dialog } from "@ada-cx/lovelace";

<Dialog>
  <Dialog.Icon intent="warning" />
  <Dialog.Content>
    <Dialog.Title>Delete conversation?</Dialog.Title>
    <Dialog.Body>This can't be undone.</Dialog.Body>
  </Dialog.Content>
  <Dialog.Actions>
    <Button destructive>Delete</Button>
    <Button variant="tertiary">Cancel</Button>
  </Dialog.Actions>
</Dialog>;
```

### DialogOverlay

A centered modal dialog presentation with a scrim. Key props: `aria-describedby`, `aria-label`, `isDismissable`, `isOpen`, `onOpenChange`, `role` (`"alertdialog"` | `"dialog"`).

For `role="alertdialog"`, set an `id` on `Dialog.Body`. Pass that ID as `aria-describedby`.

```tsx
import { Button, Dialog, DialogOverlay } from "@ada-cx/lovelace";
import { useState } from "react";

function ConfirmDelete() {
  const [open, setOpen] = useState(false);
  return (
    <DialogOverlay isOpen={open} onOpenChange={setOpen} isDismissable>
      <Dialog>
        <Dialog.Content>
          <Dialog.Title>A modal dialog</Dialog.Title>
          <Dialog.Body>
            Presented by DialogOverlay with a scrim and a focus trap.
          </Dialog.Body>
        </Dialog.Content>
        <Dialog.Actions>
          <Button onPress={() => setOpen(false)}>Done</Button>
        </Dialog.Actions>
      </Dialog>
    </DialogOverlay>
  );
}
```

### FullscreenOverlay

A modal surface that fills the window. Key props: `aria-label`, `isDismissable`, `isOpen`, `onOpenChange`.

Name the dialog with a `Sheet.Title` inside it or with `aria-label`.

```tsx
import { Button, FullscreenOverlay } from "@ada-cx/lovelace";
import { useState } from "react";

function FullscreenDemo() {
  const [open, setOpen] = useState(false);
  return (
    <FullscreenOverlay aria-label="Fullscreen demo" isOpen={open} onOpenChange={setOpen} isDismissable>
      <p>A surface that fills the window.</p>
      <Button onPress={() => setOpen(false)}>Close</Button>
    </FullscreenOverlay>
  );
}
```

### Menu and MenuItem

`Menu` groups rows with arrow-key navigation and text search. `MenuItem` renders one row. Key props for `Menu`: `aria-label`, `children`, `defaultSelectedKeys`, `onAction`, `selectionMode`. Key props for `MenuItem`: `children`, `id`, `isDisabled`, `onAction`, `textValue`.

Parts:

* `MenuItem.Checkbox` shows a checkbox selection indicator. Key props: `className`, `ref`.
* `MenuItem.Label` supplies the row's text. Key props: `children`.
* `MenuItem.Radio` shows a radio selection indicator. Key props: `className`, `ref`.
* `MenuItem.Toggle` shows a toggle selection indicator. Key props: `className`, `ref`.

Place each selection indicator directly inside `MenuItem`. Set `selectionMode="multiple"` for checkboxes and toggles, or `selectionMode="single"` for radio indicators.

```tsx
import { Menu, MenuItem } from "@ada-cx/lovelace";

<>
  <Menu aria-label="Conversation actions" onAction={handleAction}>
    <MenuItem>
      <MenuItem.Label>Email transcript</MenuItem.Label>
    </MenuItem>
    <MenuItem>
      <MenuItem.Label>Mute sounds</MenuItem.Label>
    </MenuItem>
    <MenuItem>
      <MenuItem.Label>End chat</MenuItem.Label>
    </MenuItem>
  </Menu>
  <Menu aria-label="Conversation settings" defaultSelectedKeys={["sound"]} selectionMode="multiple">
    <MenuItem id="transcript">
      <MenuItem.Checkbox />
      <MenuItem.Label>Email transcript</MenuItem.Label>
    </MenuItem>
    <MenuItem id="sound">
      <MenuItem.Toggle />
      <MenuItem.Label>Sound on</MenuItem.Label>
    </MenuItem>
  </Menu>
  <Menu aria-label="Contact method" defaultSelectedKeys={["chat"]} selectionMode="single">
    <MenuItem id="chat">
      <MenuItem.Radio />
      <MenuItem.Label>Continue in chat</MenuItem.Label>
    </MenuItem>
    <MenuItem id="email">
      <MenuItem.Radio />
      <MenuItem.Label>Switch to email</MenuItem.Label>
    </MenuItem>
  </Menu>
</>;
```

### Sheet

Bottom-sheet content groups a header, body, and actions. Key props: `children`, `className`, `style`. Place the header children in this order: `Sheet.BackButton`, `Sheet.Title`, `Sheet.CloseButton`. Wrap the content in `SheetOverlay` for modal behavior.

Parts:

* `Sheet.Actions` stacks the action buttons. Key props: `children`, `className`.
* `Sheet.BackButton` adds the header's back button. Key props: `className`, `label`, `onPress`.
* `Sheet.Body` holds the scrollable content. Key props: `children`, `className`, `id`, `keyboardScrollLabel`.
* `Sheet.CloseButton` adds the header's close button. Key props: `className`, `label`, `onPress`.
* `Sheet.Header` groups the title and buttons. Key props: `children`, `className`.
* `Sheet.Title` supplies the dialog's accessible name. Key props: `children`, `className`.

`Sheet.Header` takes its content as children. The earlier `backLabel`, `closeLabel`, `onBack`, `onClose`, `showBack`, `showClose`, and `title` props are removed. Render `Sheet.Title` for the title, and `Sheet.BackButton` or `Sheet.CloseButton` for the edge buttons.

```tsx
import { Button, Sheet } from "@ada-cx/lovelace";

<Sheet>
  <Sheet.Header>
    <Sheet.BackButton onPress={goBack} />
    <Sheet.Title>Email transcript</Sheet.Title>
    <Sheet.CloseButton onPress={close} />
  </Sheet.Header>
  <Sheet.Body>
    <p>Enter an email address to receive a copy of this conversation.</p>
  </Sheet.Body>
  <Sheet.Actions>
    <Button>Send</Button>
  </Sheet.Actions>
</Sheet>;
```

### SheetOverlay

A dimming, slide-up modal bottom-sheet presentation. Key props: `aria-describedby`, `aria-label`, `isDismissable`, `isOpen`, `maxHeight`, `onOpenChange`.

`maxHeight` defaults to 80% of the available height. The widget sheet uses 100%.

```tsx
import { Sheet, SheetOverlay } from "@ada-cx/lovelace";
import { useState } from "react";

function TranscriptSheet() {
  const [open, setOpen] = useState(false);
  return (
    <SheetOverlay isOpen={open} onOpenChange={setOpen} isDismissable>
      <Sheet>
        <Sheet.Header>
          <Sheet.Title>A bottom sheet</Sheet.Title>
          <Sheet.CloseButton onPress={() => setOpen(false)} />
        </Sheet.Header>
        <Sheet.Body>
          <p>Presented by SheetOverlay: it dims and slides up.</p>
        </Sheet.Body>
      </Sheet>
    </SheetOverlay>
  );
}
```

### Tooltip and TooltipTrigger

`Tooltip` displays help text with a directional arrow. `TooltipTrigger` associates it with a focusable control. Key props for `Tooltip`: `children`, `placement` (`"bottom"` | `"left"` | `"right"` | `"top"`). Key props for `TooltipTrigger`: `children`, `delay`.

```tsx
import { Button, Tooltip, TooltipTrigger } from "@ada-cx/lovelace";

<TooltipTrigger delay={0}>
  <Button variant="secondary" size="sm">
    Hover me
  </Button>
  <Tooltip placement="top">Tooltips point at their trigger</Tooltip>
</TooltipTrigger>;
```

## Primitives

Low-level building blocks the other components compose.

### Avatar

A circular avatar bubble holding initials, an `Icon`, or an `Image`. Key props: `aria-hidden`, `children`, `label`, `showAvatar`, `size` (`"lg"` | `"md"` | `"sm"` | `"xl"`).

Pass exactly one: `label` to name the avatar, or `aria-hidden={true}` to mark it decorative.

```tsx
import { Avatar, Image } from "@ada-cx/lovelace";

<>
  <Avatar size="lg" label="Ada AI Agent">
    A
  </Avatar>
  <Avatar size="md" label="Support agent">
    <Image src="https://example.com/agent.jpg" alt="" />
  </Avatar>
</>;
```

### AvatarPlaceholder

An empty avatar bubble for loading or anonymous states. Key props: `size`.

```tsx
import { AvatarPlaceholder } from "@ada-cx/lovelace";

<AvatarPlaceholder size="md" />;
```

### Icon

Wraps your own SVG glyph and standardizes its size and accessible name. Exported glyphs already wrap themselves. Pass `label` to a glyph directly. Key props: `children`, `label`.

```tsx
import { Icon } from "@ada-cx/lovelace";

<Icon label="Star">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <path d="M12 2l3 7h7l-5.5 4.5L18 21l-6-4-6 4 1.5-7.5L2 9h7z" />
  </svg>
</Icon>;
```

### Icon glyphs

The package exports glyph components, including `ArrowDown`, `ArrowUp`, `Checkmark`, `ChevronLeft`, `CloudUpload`, `Email`, `Paperclip`, `Send`, `ThumbsDown`, and `ThumbsUp`. Each glyph wraps itself for correct sizing. Pass a glyph directly wherever a component accepts an icon. Key props: `label`. Each glyph is decorative by default. Set `label` when the glyph needs its own accessible name.

```tsx
import { IconButton, Send } from "@ada-cx/lovelace";

<IconButton label="Send message" onPress={send}>
  <Send />
</IconButton>;
```

`ArrowUp` is a separate upward arrow.

The preview includes every exported glyph:

`AdaLogo`, `ArrowDown`, `ArrowLeft`, `ArrowNortheast`, `ArrowNorthwest`, `ArrowRight`, `ArrowUp`, `Bell`, `BellSlash`, `Checkmark`, `CheckmarkCircle`, `ChevronLeft`, `ChevronRight`, `CloudUpload`, `Document`, `Download`, `Ellipses`, `Email`, `ExclamationSync`, `ExclamationTriangle`, `Eye`, `EyeSlash`, `Language`, `MediaSlash`, `MessagingFill`, `Minimize`, `Mute`, `Paperclip`, `PersonFill`, `PlayFill`, `Send`, `StarBadgeFill`, `ThumbsDown`, `ThumbsUp`, `Volume`, `Xmark`, `XmarkCircle`.

### Image

An image with load-state tracking and a fallback slot. Key props: `alt`, `fallback`, `onLoadingStatusChange`, `src`. Supply `alt` for every image.

```tsx
import { Image } from "@ada-cx/lovelace";

<Image src="https://example.com/photo.jpg" alt="Order photo" />;
```

### LovelaceProvider

Applies a color scheme and token overrides to its children, including overlays. Key props: `children`, `override`, `theme` (`"auto"` | `"dark"` | `"light"`). The default `"auto"` theme follows the system color scheme.

```tsx
import { Button, LovelaceProvider } from "@ada-cx/lovelace";

<LovelaceProvider theme="light" override={{ "color-background-accent": "#0369a1" }}>
  <Button>Send message</Button>
</LovelaceProvider>;
```

### Text

Text at one of the four semantic type tiers. Key props: `as`, `variant` (`"body"` | `"caption"` | `"title-large"` | `"title-small"`), `weight` (`"medium"` | `"regular"` | `"semi-bold"`).

`Text` renders a `<span>` by default. Set `as` to render a different element. Do not use `Text` for a heading, because a heading gets its level from its role. Use the React Aria `Heading` component instead, as `Dialog.Title` does.

```tsx
import { Text } from "@ada-cx/lovelace";

<>
  <Text variant="title-large" as="p">
    Order #1042
  </Text>
  <Text variant="caption">Delivered on May 3</Text>
</>;
```