> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Details Summary Label and Attributes

> Guide for adopting the Details.Summary label prop, label sizes, and attribute cells.

Unreleased

> [!NOTE]
>
> This guide covers changes to the **beta** Details component. These APIs may
> continue to evolve before the stable release.

## Overview

The beta `Details.Summary` replaces `children` with a `label` string and lays the label and its attribute cells out on a shared grid:

* **`label`** - The summary text. It supports `*italic*` and `==highlight==` markup.
* **`labelProps`** - Sets the label's text `size` and adds an optional `moreInfo` tooltip.
* **`attributes`** - Adds text, chip, button, and menu cells after the label.
* **`templateColumns`** - Defines the grid tracks the label and cells share.
* **`gap`** - Sets the space between those tracks.

These changes reflect our design philosophy of **constrained content**. Rather than accepting arbitrary React elements in the summary, the new API accepts a string and a fixed set of cells, so every summary keeps the same typography, alignment, and click behavior.

## Design Rationale

### Why `label` instead of `children`?

Previously, `children` accepted any React element, so each summary styled its own content:

```tsx theme={null}
// Before: Any element is accepted, so typography and alignment vary
<Details.Summary>
  <Flex gap="2" alignItems="center">
    <span>Job pricing</span>
    <Chip label="Open" />
  </Flex>
</Details.Summary>
```

The new approach takes a string for the label and moves everything else into typed cells:

```tsx theme={null}
// After: A string label and typed cells
<Details.Summary
  label="Job pricing"
  templateColumns="1fr auto"
  attributes={[{ type: "chip", label: "Open" }]}
/>
```

The label renders as `Text` with the summary's original typography, so a plain label looks the same as it did with plain-text `children`.

### Why a grid?

The label and the cells are siblings in one grid, so a single `templateColumns` value sets the width of each. The label takes the first track, and each cell takes the next one. Use the same proportions as the content below to keep each cell near its column. The grid sits inside the summary's padding, so the cells are offset by a few pixels from the content's own columns.

## Label Reference

The label is bold and uses the display font by default. Two inline markers are supported:

| Syntax | Result |
| - | - |
| `*text*` | *Italic* |
| `==text==` | Highlighted |

Any other marker, including `**bold**` and backticks, renders as literal text. The label is already bold, so bold markup is not needed.

```tsx theme={null}
<Details.Summary label="Open *estimates* from ==this week==" />
```

### Label sizes

Use `labelProps.size` to change the label's text scale. The expand arrow scales with the label at 1.5 times its font size.

| Size | Label font size |
| - | - |
| `small` | `0.875rem` |
| `medium` | `1rem` |
| `large` | `1.25rem` |
| `xlarge` | `1.5rem` |

`medium` is the default.

```tsx theme={null}
<Details.Summary label="Job pricing" labelProps={{ size: "large" }} />
```

### More info

Set `labelProps.moreInfo` to a string to add an info button right after the label. Hovering or focusing the button shows the string in a tooltip.

```tsx theme={null}
<Details.Summary
  label="Job pricing"
  labelProps={{ moreInfo: "Totals include parts, labor, and permits." }}
/>
```

The info button sits beside the toggle button, not inside it. Pressing it opens the tooltip and does not expand or collapse the Details, and keyboard users reach it with one Tab after the toggle. The button scales with `labelProps.size`. `moreInfo` takes a string only.

### Best practice: use sentence case

Write labels in sentence case: capitalize only the first letter of the first word, plus proper nouns. Sentence case keeps labels easy to scan and matches the rest of Anvil2. The expand arrow aligns to the height of capital letters, so a label that starts with a capital letter sits most evenly beside it.

```tsx theme={null}
// Good
<Details.Summary label="Open estimates" />

// Avoid
<Details.Summary label="open estimates" />
<Details.Summary label="Open Estimates" />
```

## Attributes Reference

`attributes` takes an array of cells. Each cell has a `type` and renders after the label in the order given. Cells are aligned to the end of their track and do not wrap.

| Type | Props | Renders |
| - | - | - |
| `text` | `label`, `value`, `size` | `label: value` with the value in bold |
| `chip` | `label`, `color` | A `Chip` |
| `button` | `label`, `appearance`, `icon`, `onClick` | A small, labeled `Button` |
| `menu` | `label`, `items` | A small `Menu` button that opens a flat list |

### Text cells

`label` and `value` are both optional. The colon appears only when both are set, and an empty string counts as not set. `size` takes the same values as the label and defaults to `medium`.

```tsx theme={null}
attributes={[
  { type: "text", label: "Total", value: "$4,820.00" },
  { type: "text", label: "Reviewed" },
  { type: "text", value: "3 items" },
]}
```

### Chip cells

`label` and `color` are the `Chip` props of the same names.

```tsx theme={null}
attributes={[{ type: "chip", label: "Open", color: "#cce3ff" }]}
```

### Button cells

A button cell is always a labeled button, so there is no icon-only form. `appearance` defaults to `secondary`. A bare `icon` renders before the label. Use `{ after: Icon }` to place it after.

```tsx theme={null}
attributes={[
  { type: "button", label: "Add item", icon: Add, onClick: addItem },
  {
    type: "button",
    label: "Next",
    icon: { after: ArrowForward },
    appearance: "primary",
    onClick: next,
  },
]}
```

### Menu cells

A menu cell takes `items`, each with a `label`, an optional `onClick`, and an optional `disabled`. Selecting an item runs its `onClick` and closes the menu. Submenus are not supported.

```tsx theme={null}
attributes={[
  {
    type: "menu",
    label: "Actions",
    items: [
      { label: "Edit", onClick: edit },
      { label: "Duplicate", onClick: duplicate },
      { label: "Delete", disabled: true },
    ],
  },
]}
```

### Placing cells on the grid

`templateColumns` defaults to `1fr`, which gives the label the whole row. Add one track for each cell:

```tsx theme={null}
<Details.Summary
  label="Job pricing"
  templateColumns="1fr auto auto"
  gap="4"
  attributes={[
    { type: "text", label: "Total", value: "$4,820.00" },
    { type: "button", label: "Review", onClick: review },
  ]}
/>
```

Size a track wider for a cell that needs more room. A cell beyond the last track wraps to a new row. `gap` takes the same values as the `Grid` component.

### Click behavior

The whole summary row toggles the Details. Clicking the label, a text or chip cell, or empty space in the row opens or closes it. Button and menu cells sit beside the toggle button, not inside it. They run only their own action and do not toggle: pressing a button, opening a menu, or choosing a menu item leaves the Details as it is.

Keyboard users toggle the Details with the toggle button, and tab to each button or menu cell as a separate control. The toggle button's accessible name is the label alone.

### Styling the summary

`className` and `style` apply to the summary row, so they style the whole summary: background, padding, border, and radius. Every other prop, including `ref`, `onClick`, `aria-*`, and `data-*`, goes to the toggle button.

Override the summary's colors with custom properties on the row, set through `style`:

| Custom property | Controls |
| - | - |
| `--a2-mod-details-summary-background-color` | Background |
| `--a2-mod-details-summary-background-color-hover` | Background on hover |
| `--a2-mod-details-summary-background-color-active` | Background while pressed |
| `--a2-mod-details-summary-foreground-color` | Text color |
| `--a2-mod-details-summary-foreground-color-hover` | Text color on hover |
| `--a2-mod-details-summary-foreground-color-active` | Text color while pressed |
| `--a2-mod-details-summary-focus-ring-color` | Focus ring around the row |

```tsx theme={null}
import type { CSSProperties } from "react";

const summaryStyle = {
  "--a2-mod-details-summary-background-color": "#f5f5f5",
} as CSSProperties;

<Details.Summary label="Job pricing" style={summaryStyle} />;
```

Prefer `style` for these properties. A class with a single selector can lose to the component's own rule.

The focus ring is drawn around the whole row when the toggle button has keyboard focus. To style focus from your own `className`, use `:has([data-anv="details-summary"]:focus-visible)` on the row. Avoid `:focus-within`, which also matches when a button or menu cell in the row has focus.

## Migration Guide

### Plain text summaries

```tsx theme={null}
// Before
<Details.Summary>Job pricing</Details.Summary>

// After
<Details.Summary label="Job pricing" />
```

### Summaries with inline emphasis

```tsx theme={null}
// Before
<Details.Summary>
  Open <em>estimates</em>
</Details.Summary>

// After
<Details.Summary label="Open *estimates*" />
```

### Summaries with extra elements

Move chips, buttons, and values out of `children` and into `attributes`:

```tsx theme={null}
// Before
<Details.Summary>
  <Flex gap="2" alignItems="center">
    <span>Job pricing</span>
    <Chip label="Open" />
    <Button size="small" onClick={review}>
      Review
    </Button>
  </Flex>
</Details.Summary>

// After
<Details.Summary
  label="Job pricing"
  templateColumns="1fr auto auto"
  gap="4"
  attributes={[
    { type: "chip", label: "Open" },
    { type: "button", label: "Review", onClick: review },
  ]}
/>
```

### Summaries with a different text size

```tsx theme={null}
// Before: Set the size on the content
<Details.Summary>
  <span style={{ fontSize: "1.25rem" }}>Job pricing</span>
</Details.Summary>

// After
<Details.Summary label="Job pricing" labelProps={{ size: "large" }} />
```

## Breaking Changes

> [!WARNING]
>
> These changes require code updates when you move to the beta `Details`. The
> stable `Details` is unchanged.

The following changes apply to the beta `Details.Summary`:

* `children` has been **removed** - Use `label` instead.
* `className` and `style` now apply to the **summary row**, not the `<button>` element - A rule that targeted the button itself, such as its `display` or `:focus-visible`, needs to target the row. Overrides that set padding, background, or the `--a2-mod-details-summary-*` custom properties work unchanged.
* The label **renders as `Text`** - Content that relied on custom elements, fonts, or colors inside the summary must move to `attributes` cells or be dropped.

### Why Breaking?

The point of the new API is a constrained summary. Accepting `children` alongside `label` would keep the arbitrary content the change removes, and beta status allows the API to change.

## New Exports

The following are now exported from `@servicetitan/anvil2/beta`:

* `Details` - The disclosure container, with `Details.Summary` and `Details.Content`.
* `DetailsProps` - Props for `Details`.
* `DetailsSummaryProps` - Props for `Details.Summary`.
* `DetailsSummaryLabelProps` - The `labelProps` options.
* `DetailsSummaryColumnAttribute` - An attribute cell.
* `DetailsSummaryTextAttribute` - A text cell.
* `DetailsSummaryChipAttribute` - A chip cell.
* `DetailsSummaryButtonAttribute` - A button cell.
* `DetailsSummaryMenuAttribute` - A menu cell.
* `DetailsSummaryMenuItem` - An item in a menu cell.
* `DetailsContentProps` - Props for `Details.Content`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.