# Item

## Usage

A single, flexible item primitive that unifies the "start content + label + description + end content" pattern across Astryx. Use it wherever you need a structured row: dropdown menus, selectors, contact lists, notifications, file browsers, and activity feeds.

```ts
import { Item } from '@astryx-vue/core/List'
```

Everything else that looks like a row is built from this one. [List Item](/en/components/list-item) adds the list element and the marker; menus, selectors, and result rows wrap it with the role their parent owns. The row paints nothing beyond text and slots, so the surrounding surface decides how it looks.

### Anatomy

- **Marker**: Optional list bullet/counter rendered before start content.
- **Start content**: Leading visual: avatar, icon, image, or checkbox.
- **Label**: Primary text identifying the item.
- **Description**: Secondary supporting text below the label.
- **End content**: End-aligned content: badges, timestamps, or action buttons.

## Best practices

| Guidance | Practice |
| --- | --- |
| Do | Use named slots (`startContent`, `label`, `description`, `endContent`) for the common layout. These cover the 80% case. |
| Do | Use `density="compact"` for menus and dense lists, `"balanced"` for standard rows, and `"spacious"` for roomier layouts. |
| Do | Set `labelLines` and `descriptionLines` to control truncation when content length varies. |
| Do | Use `align="start"` when start or end content is taller than a single line of text. |
| Do | Give a swipe action a second home in the row (a menu or a button in `endContent`): the swipe is a touch accelerator, and a verb reachable only by swipe is unreachable by keyboard and by mouse. |
| Avoid | Don't nest interactive elements (buttons, links) inside an interactive `Item`; it creates confusing focus and click targets. |
| Avoid | Don't use `Item` for navigation between views; use proper navigation components instead. |
| Avoid | Don't add read/unread or inbox-specific behavior directly; compose a thin wrapper like PreviewItem instead. |

## Examples

Common configurations, variations, and states.

### Basic

A basic item with a label, supporting description, and end-aligned timestamp. Use this for simple rows that need consistent text alignment and spacing.

```vue
<script setup lang="ts">
import { Item } from '@astryx-vue/core/List'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <div class="item-basic">
    <Item label="Quarterly planning" description="Agenda, notes, and action items">
      <template #endContent>
        <Text color="secondary">
          Today
        </Text>
      </template>
    </Item>
    <Item label="Customer research" description="Interview notes from the latest study">
      <template #endContent>
        <Text color="secondary">
          Yesterday
        </Text>
      </template>
    </Item>
    <Item label="Launch checklist" description="Remaining tasks before release">
      <template #endContent>
        <Text color="secondary">
          Fri
        </Text>
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-basic {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

### Document Tabs

Single-row, horizontally scrollable document switcher with compact selected rows, leading file icons, instant reveal-on-hover close actions with reserved geometry, stable separators, and an add control outside the scroll area. Use for editor tabs, workspace tabs, open documents, files, canvases, or panels.

```vue
<script setup lang="ts">
import { Divider } from '@astryx-vue/core/Divider'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { Item } from '@astryx-vue/core/List'
import { HStack } from '@astryx-vue/core/Stack'

const tabs = [
  { name: 'Project brief', isActive: true },
  { name: 'Research notes', isActive: false },
  { name: 'Interaction prototype', isActive: false },
]

function onSelect(): void {}
function onClose(): void {}
function onAdd(): void {}
</script>

<template>
  <HStack align="center" class="item-tabs" :gap="0.5">
    <HStack
      align="center"
      class="item-tabs__strip"
      role="group"
      aria-label="Open documents"
      :gap="0.5"
      :padding-inline="2"
      :padding-block="1"
      is-scrollable
    >
      <template v-for="(tab, index) in tabs" :key="tab.name">
        <Divider v-if="index > 0" orientation="vertical" class="item-tabs__rule" />
        <Item
          class="item-tabs__tab"
          :label="tab.name"
          density="compact"
          :is-selected="tab.isActive"
          :aria-current="tab.isActive ? 'true' : undefined"
          @click="onSelect"
        >
          <template #startContent>
            <Icon
              icon="i-carbon-document"
              size="sm"
              :color="tab.isActive ? 'primary' : 'secondary'"
            />
          </template>
          <template #endContent>
            <IconButton
              class="item-tabs__close"
              :label="`Close ${tab.name}`"
              variant="ghost"
              size="sm"
              @click.stop="onClose"
            >
              <Icon icon="close" size="sm" />
            </IconButton>
          </template>
        </Item>
      </template>
    </HStack>
    <IconButton label="New document" variant="ghost" size="sm" @click="onAdd">
      <Icon icon="i-carbon-add" size="sm" color="secondary" />
    </IconButton>
  </HStack>
</template>

<style scoped>
.item-tabs {
  min-width: 0;
  width: min(100%, 600px);
}

.item-tabs__strip {
  min-width: 0;
  scrollbar-width: none;
}

.item-tabs__rule {
  height: 16px;
}

.item-tabs__tab {
  flex: 0 1 auto;
  width: 164px;
  min-width: 96px;
}

/* The close button stays in the layout and only shows on hover or focus, so the
   label never shifts when the pointer enters the tab. */
.item-tabs__close {
  visibility: hidden;
}

.item-tabs__tab:hover .item-tabs__close,
.item-tabs__tab:focus-within .item-tabs__close,
.item-tabs__tab[data-selected] .item-tabs__close {
  visibility: visible;
}
</style>
```

### Metadata

Items with end-aligned metadata and badges. Use the endContent slot for counts, status, timestamps, and other secondary row information.

```vue
<script setup lang="ts">
import { Badge } from '@astryx-vue/core/Badge'
import { Icon } from '@astryx-vue/core/Icon'
import { Item } from '@astryx-vue/core/List'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <div class="item-metadata">
    <Item label="Build passed" description="Production deploy completed">
      <template #startContent>
        <Icon icon="check" size="sm" color="success" />
      </template>
      <template #endContent>
        <Text color="secondary">
          2m ago
        </Text>
      </template>
    </Item>
    <Item label="High memory usage" description="Worker pool is above the warning threshold" is-highlighted>
      <template #startContent>
        <Icon icon="warning" size="sm" color="warning" />
      </template>
      <template #endContent>
        <Badge label="Warning" variant="warning" />
      </template>
    </Item>
    <Item label="Sync failed" description="Retry after checking service credentials" is-selected>
      <template #startContent>
        <Icon icon="error" size="sm" color="error" />
      </template>
      <template #endContent>
        <Badge label="Action" variant="error" />
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-metadata {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

### Start Content

Items with leading avatars and icons in the startContent slot. Keep start content small so the row stays compact and easy to scan.

```vue
<script setup lang="ts">
import { Avatar } from '@astryx-vue/core/Avatar'
import { Badge } from '@astryx-vue/core/Badge'
import { Icon } from '@astryx-vue/core/Icon'
import { Item } from '@astryx-vue/core/List'
import { Text } from '@astryx-vue/core/Text'

function onSelect(): void {}
</script>

<template>
  <div class="item-start-content">
    <Item label="Ada Lovelace" description="Design systems engineer" @click="onSelect">
      <template #startContent>
        <Avatar name="Ada Lovelace" size="sm" />
      </template>
      <template #endContent>
        <Badge label="Owner" variant="purple" />
      </template>
    </Item>
    <Item label="Grace Hopper" description="Compiler platform" @click="onSelect">
      <template #startContent>
        <Avatar name="Grace Hopper" size="sm" />
      </template>
      <template #endContent>
        <Text color="secondary">
          Online
        </Text>
      </template>
    </Item>
    <Item label="Review handoff notes" description="Updated guidance is ready for the team" @click="onSelect">
      <template #startContent>
        <Icon icon="info" size="sm" color="secondary" />
      </template>
      <template #endContent>
        <Badge label="New" variant="blue" />
      </template>
    </Item>
  </div>
</template>

<style scoped>
.item-start-content {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
</style>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string \| number` | — | Primary text of the row. A string or a number truncates to one line; use the `label` slot for rich content. |
| `description` | `string \| number` | — | Secondary text below the label, also truncated to one line. Use the `description` slot for several lines or rich content. |
| `as` | `ItemElement` (tag name or component) | `'div'` | What the root renders as. Give a component only when the row carries a `role`, so the parent owns keyboard access and the row adds no second tab stop, and only when no control sits in `startContent` or `endContent`. |
| `align` | `'center' \| 'start'` | `'center'` | Vertical alignment of the start and end content slots. |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | Spacing. `compact` uses 4px block padding, `balanced` 8px, and `spacious` 12px of block and inline padding. |
| `layout` | `'stacked' \| 'inline'` | `'stacked'` | `stacked` puts the description on its own line. `inline` keeps both on one line, with the description ellipsizing first, so the row fits a fixed-height host. |
| `labelLines` | `number` | — | Maximum lines before the label truncates with an ellipsis. A string label truncates to one line when this is omitted. |
| `descriptionLines` | `number` | — | Maximum lines before the description truncates with an ellipsis. A string description truncates to one line when this is omitted. |
| `onClick` | `(event: MouseEvent) => void` | — | Click handler. It renders a button that fills the row's content area, so the row gains button semantics and the hover and pressed states. Write `@click` in a template. |
| `interactiveRef` | `MaybeRefOrGetter` | — | A ref to a control inside the row that already carries its keyboard access and action, such as a checkbox in `startContent`. The row becomes an enlarged click target that forwards surface clicks to that control and renders no button of its own. Mutually exclusive with `onClick` and `href`, which are ignored while it is set. |
| `href` | `string` | — | Link URL. A link element is rendered inside the row. When the root is already a link component (see `as`), the address goes on that root instead and no extra element is drawn. |
| `target` | `'_blank' \| '_self'` | — | Link target, only used with `href`. `_blank` adds `noopener` and `noreferrer` automatically. |
| `rel` | `string` | — | Link relationship tokens, merged with the ones added automatically. |
| `isDisabled` | `boolean` | `false` | Disabled state. The row becomes inert and its content dims. |
| `isSelected` | `boolean` | `false` | Selected state. Written as `aria-selected="true"` when the row's role permits it, and as `aria-current="true"` otherwise. |
| `isHighlighted` | `boolean` | `false` | Paints the highlighted state, which is the appearance keyboard focus and hover use. |
| `swipeActions` | `ItemSwipeActions` | — | Verbs a sideways touch drag uncovers, per side, outermost last, each `{ label, icon?, onActivate, isDisabled?, variant?, hasRemoval? }`. A mouse never starts the drag. Only on a row whose role permits interactive descendants, not on an option, a menu item, or the enlarged target of a native radio. |
| `swipeBehavior` | `'reveal' \| 'commit'` | `'reveal'` | `reveal` rests the row open with every entry a real button and fires the outermost on a long drag; `commit` slides the row out and fires on release, with nothing left resting. |

## Slots

- `marker`: content before `startContent`, such as a list bullet or a counter.
- `startContent`: leading visual, such as an avatar, an icon, an image, or a checkbox.
- `label`: rich content replacing the `label` prop.
- `description`: rich content replacing the `description` prop.
- `endContent`: end-aligned content, such as badges, timestamps, or action buttons.

## Exposed

`element` (the root element), `focus()`, and `blur()`. Focusing lands on the control inside the row that can actually take focus.
