# InboxItem

> The row for something that happened elsewhere and that the person should see — a mention, an approval, a hand-off — with an attention dot while it is unread and its actions in the row.

Source: https://raft-design.botiverse.build/components/inbox-item. Generated from the current release; where it differs from what you remember, this page is right.

An InboxItem is one thing that happened elsewhere and that the person should see: someone mentioned them, an agent waits for their approval, work was handed to them. An unread item has a bold title and a pink attention dot; an action it needs sits in the row. It is the row of [ActivityInbox](https://raft-design.botiverse.build/components/activity-inbox.md).

```tsx
import {
  AtSign,
  BellOff,
  Bookmark,
  Check,
  Circle,
  Hash,
  MessageSquare,
  Pencil,
  RotateCcw,
} from "lucide-react";
import {
  Badge,
  Checkbox,
  InboxItem,
  InboxItemActions,
  InboxItemBody,
  InboxItemContent,
  InboxItemHeading,
  InboxItemMetadata,
  InboxItemSelection,
  InboxItemSender,
  InboxItemSubtitle,
  InboxItemTimestamp,
  InboxItemTitle,
  InboxItemTrailing,
  PanelAction,
  PanelToggleAction,
} from "@botiverse/raft-ui";
import { useCallback, useState } from "react";

import { ACTIVITY_COMPOSITION_ITEMS } from "../activity.fixtures.ts";
import type { ActivityCompositionItem } from "../activity.fixtures.ts";

const ACTIVE_ITEM_ID = "dm:samira";
const CHECKED_ITEM_ID = "mention:general";
const ACTIVITY_ITEM_STATES: readonly ActivityCompositionItem[] = ACTIVITY_COMPOSITION_ITEMS;

/** Static gallery of Activity inbox item structures and visual states. */
export function ActivityPreviewComposition() {
  return (
    <div className="flex w-full max-w-5xl flex-col gap-2">
      {ACTIVITY_ITEM_STATES.map((item) => {
        const Icon =
          item.kind === "thread" ? MessageSquare : item.channelKind === "dm" ? AtSign : Hash;
        const active = item.id === ACTIVE_ITEM_ID;
        const checked = item.id === CHECKED_ITEM_ID;

        return (
          <InboxItem key={item.id} active={active} data-unread={item.unreadCount > 0 || undefined}>
            <InboxItemSelection>
              <Checkbox defaultChecked={checked} aria-label="Toggle activity item selection" />
            </InboxItemSelection>

            <InboxItemContent>
              <InboxItemHeading>
                <InboxItemTitle>
                  <Icon aria-hidden />
                  {item.title}
                </InboxItemTitle>
              </InboxItemHeading>
              {item.kind === "thread" ? (
                <InboxItemSubtitle>Thread in {item.channelLabel}</InboxItemSubtitle>
              ) : null}
              <InboxItemBody>
                <InboxItemSender>{item.sender}: </InboxItemSender>
                {item.preview}
              </InboxItemBody>
              <InboxItemMetadata>
                {item.task ? (
                  <Badge variant="warning">
                    <Circle aria-hidden size={10} />#{item.task.number}
                    {item.task.claimedBy ? ` @${item.task.claimedBy}` : null}
                  </Badge>
                ) : null}
                {item.replyCount === undefined ? null : (
                  <Badge appearance="outline" variant="muted">
                    {item.replyCount} replies
                  </Badge>
                )}
                {item.kind === "thread" && item.following === false ? (
                  <Badge appearance="outline" variant="muted">
                    <BellOff aria-hidden size={10} /> Unfollowed
                  </Badge>
                ) : null}
                {item.hasMention && item.unreadCount > 0 ? (
                  <Badge variant="primary">
                    <AtSign aria-hidden size={10} /> Mention
                  </Badge>
                ) : null}
                {item.unreadCount > 0 ? (
                  <Badge variant="accent">{item.unreadCount} new</Badge>
                ) : null}
                {item.draft ? (
                  <Badge appearance="outline" variant="muted">
                    <Pencil aria-hidden size={10} /> Draft
                  </Badge>
                ) : null}
              </InboxItemMetadata>
            </InboxItemContent>

            <InboxItemTrailing>
              <InboxItemTimestamp>{item.timestamp}</InboxItemTimestamp>
              <InboxItemActions>
                <ActivitySavedAction defaultSaved={Boolean(item.saved)} />
                <ActivityCompletionAction defaultDone={Boolean(item.done)} />
              </InboxItemActions>
            </InboxItemTrailing>
          </InboxItem>
        );
      })}
    </div>
  );
}

function ActivitySavedAction({ defaultSaved }: { defaultSaved: boolean }) {
  const [saved, setSaved] = useState(defaultSaved);

  return (
    <PanelToggleAction
      pressed={saved}
      onPressedChange={setSaved}
      aria-label={saved ? "Remove from Saved" : "Save activity item"}
    >
      <Bookmark aria-hidden fill={saved ? "currentColor" : "none"} />
    </PanelToggleAction>
  );
}

function ActivityCompletionAction({ defaultDone }: { defaultDone: boolean }) {
  const [done, setDone] = useState(defaultDone);

  const handleToggleDone = useCallback(() => {
    setDone((current) => !current);
  }, []);

  return (
    <PanelAction
      onClick={handleToggleDone}
      aria-label={done ? "Restore to Active" : "Mark as done"}
    >
      {done ? <RotateCcw aria-hidden /> : <Check aria-hidden />}
    </PanelAction>
  );
}
```

## When to use

| Situation                                                                 | Use                                                                                  |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Something happened elsewhere that the person should see                   | InboxItem, in Activity ([ActivityInbox](https://raft-design.botiverse.build/components/activity-inbox.md))        |
| The state of the whole workspace: a computer offline, a quota nearly full | A notification ([NotificationCenter](https://raft-design.botiverse.build/components/notification-center.md)) |
| The result of an action the person took                                   | [Toast](https://raft-design.botiverse.build/components/toast.md)                                                           |
| Something to read before acting, in a dialog or form                      | [Banner](https://raft-design.botiverse.build/components/banner.md)                                                        |
| A conversation, a setting or a member in a list                           | [ListRow](https://raft-design.botiverse.build/components/list-row.md)                                                   |

Which message goes where is in [Choosing a component](https://raft-design.botiverse.build/patterns/choosing-components.md#telling-people-something).

## Anatomy

- `InboxItem`: the row. Required.
- `InboxItemTrigger`: an invisible button that covers the whole row and opens the item. Required.
- `InboxItemSelection`: a [Checkbox](https://raft-design.botiverse.build/components/checkbox.md) for selecting several items. Optional.
- `InboxItemContent`: the text column. Required.
  - `InboxItemContext`: where it happened: a channel, a task, a computer.
  - `InboxItemHeading` with `InboxItemTitle`: what happened, with an optional 14px icon before it.
  - `InboxItemSubtitle`: a second line of context, such as "Thread in #launch-week".
  - `InboxItemBody` or `InboxItemMessage`: the excerpt, starting with `InboxItemSender`.
  - `InboxItemMetadata`: chips for the task, replies and mentions.
- `InboxItemTrailing`: `InboxItemTimestamp`, `InboxItemActions` and, while unread, the attention dot.

## States

| State                                 | Look                                                                           |
| ------------------------------------- | ------------------------------------------------------------------------------ |
| Unread (`data-unread` on `InboxItem`) | The title in `foreground` at weight 700, and a pink attention dot on the right |
| Read                                  | The title in `foreground-muted` at weight 600; no dot                          |
| `active`                              | The item open beside the list: an `info-soft` wash                             |
| `disabled`                            | No hover, no press; the trigger can't be pressed                               |

```tsx
import {
  ActivityInboxPanel,
  ActivityInboxViewport,
  InboxItem,
  InboxItemContent,
  InboxItemSelection,
  InboxItemTrailing,
  Skeleton,
} from "@botiverse/raft-ui";

export function LoadingActivityComposition() {
  return (
    <ActivityInboxPanel aria-busy="true">
      <ActivityInboxViewport>
        <div className="flex flex-col gap-2">
          <ActivityLoadingItem titleWidth="w-2/5" bodyWidth="w-4/5" />
          <ActivityLoadingItem titleWidth="w-1/3" bodyWidth="w-3/5" />
          <ActivityLoadingItem titleWidth="w-1/2" bodyWidth="w-3/4" />
        </div>
      </ActivityInboxViewport>
    </ActivityInboxPanel>
  );
}

function ActivityLoadingItem({ titleWidth, bodyWidth }: { titleWidth: string; bodyWidth: string }) {
  return (
    <InboxItem aria-hidden disabled>
      <InboxItemSelection>
        <Skeleton className="size-4" />
      </InboxItemSelection>
      <InboxItemContent className="flex flex-col gap-2 py-0.5">
        <Skeleton variant="line" className={titleWidth} />
        <Skeleton variant="line" className={bodyWidth} />
      </InboxItemContent>
      <InboxItemTrailing>
        <Skeleton variant="line" className="w-14" />
      </InboxItemTrailing>
    </InboxItem>
  );
}
```

## Behavior

- The trigger covers the whole row. Buttons in the row sit above it, and each one can be pressed on its own.
- Divide items with `line-hairline`, starting at the text.

## Content

- Write the context line as where it happened: #launch-week · task #25.
- Write the title as who did what, with a teammate verb: "Martin needs your OK", "Bugen handed off #25" ([Writing](https://raft-design.botiverse.build/foundations/writing.md#voice)).
- Keep the excerpt to at most 2 lines.

## Accessibility

- The parts render native elements through Base UI's `useRender`. `InboxItemTrigger` is a native `button`; with `disabled` it gets `aria-disabled` and ignores clicks.
- Give the trigger an accessible name that names the item: `aria-label="Open Martin needs your OK"`. Set `aria-current` on the trigger of the open item.
- Name the selection checkbox and every icon-only action: `aria-label="Select activity item"`, `aria-label="Mark as done"`.
- The attention dot is drawn in CSS and isn't announced. Say "unread" in the trigger's name, or add a visually hidden word.

## Rules

- Use ink-outline small buttons for actions in the row, never pink: a list can hold several items that wait for the person at once ([Button](https://raft-design.botiverse.build/components/button.md)).
- Lead with who, when there is a who: the person's or agent's avatar. A notice from the system has no who; see [NotificationCenter](https://raft-design.botiverse.build/components/notification-center.md#anatomy).

## API

### InboxItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `active` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemActions

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemBody

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemContext

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemHeading

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemMessage

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemMetadata

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemSelection

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemSender

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemSubtitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemTimestamp

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemTrailing

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### InboxItemTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.5.0 (Added): Graduated the composable `ActivityInbox` shell, sidebar, selection toolbar, and standalone `InboxItem` parts to the stable package entry point.
