---
title: "SelectionActions"
description: "A floating bar that hands a text selection to an agent."
category: Feedback
status: beta
since: 0.3.9
tags: [ai, selection, rewrite, floating]
syncStatus: reviewed
verified: true
---

## Usage

### Rewriting a Selection
The host moves the bar through `idle` → `thinking` → `streaming` → `result`; the `thinking` label shimmers.
:::TuffDemoWrapper{demo="SelectionActionsRewriteDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { useSelectionAnchor } from '@talex-touch/tuffex/selection-actions'
  import { ref } from 'vue'

  const articleRef = ref(null)
  const barRef = ref(null)
  const state = ref('idle')

  const { selection, clear } = useSelectionAnchor({
    root: articleRef,
    ignore: () => [barRef.value?.el ?? null],
  })

  function run(id) {
    state.value = 'thinking'
    // The host owns the request; call barRef.value.updatePosition() per delta while streaming.
  }
  </script>

  <template>
    <article ref="articleRef">
      <p>Pistachio holds the top slot all weekend.</p>
    </article>

    <TxSelectionActions
      ref="barRef"
      :selection="selection"
      :state="state"
      @action="run($event.id)"
      @keep="clear"
      @discard="clear"
    />
  </template>
---
:::

### Tracking the Selection
The component only presents; `useSelectionAnchor()` tracks selections in ordinary prose, and anywhere else you build a `SelectionPayload` yourself.

```ts
import { resolveSelectionPayload, useSelectionAnchor } from '@talex-touch/tuffex/selection-actions'

const { selection, clear } = useSelectionAnchor({
  root: articleRef,        // track selections inside this subtree only
  debounce: 120,           // selectionchange fires every frame while dragging
  minLength: 1,            // a selection too short to be worth a bar
  ignore: () => [barEl],   // focus landing on the bar is not a deselection
})
```

### Best Practices

- While streaming, call `updatePosition()` in the same frame as the text update, not through `setTimeout`.
- Pass the instance's `el` to `ignore`: `() => [barRef.value?.el ?? null]`. `document.querySelector` returns the wrong bar once a page has two.
- Call `clear()` after `keep` or `discard`, or the snapshot lingers and the bar never retracts.
- Scope `root` to the article container, or any selection on the page pops the bar.
- Use `TxMessageActions` for copy, regenerate, and speak at the foot of a message; this bar is only for rewriting a selection.

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `selection` | `SelectionPayload \| null` | `null` | `{ text, rects, range? }` snapshot; null retracts the bar. |
| `state` | `'idle' \| 'thinking' \| 'streaming' \| 'result'` | `'idle'` | The host owns the state machine; the component never calls a model. |
| `actions` | `SelectionActionItem[]` | Explain / Improve plus folded Shorten / Tone / Grammar | Actions as `{ id, label, more?, busyLabel? }`; give custom actions a `busyLabel`. |
| `activeActionId` | `string` | — | Id of the running action; picks the busy wording. |
| `expanded` | `boolean` | — | `v-model:expanded`, the folded action group. |
| `prompt` | `string` | — | `v-model:prompt`, the free-text instruction. |
| `hidePrompt` | `boolean` | `false` | Hides the text field and send control, for read-only surfaces. |
| `placeholder` | `string` | `'Describe edits'` | Field placeholder, also its accessible name. |
| `ariaLabel` | `string` | `'Selection actions'` | Accessible name of the bar. |
| `keepLabel` / `discardLabel` | `string` | `'Keep'` / `'Discard'` | The two result-state buttons. |
| `retryLabel` | `string` | `'Try again'` | Accessible name of the retry control. |
| `sendLabel` | `string` | `'Send edit instruction'` | Accessible name of the send control. |
| `expandLabel` / `collapseLabel` | `string` | `'Show more actions'` / `'Show fewer actions'` | Accessible names of the expand control. |
| `busyLabel` | `string` | `'Editing'` | Fallback busy wording for an action without its own. |
| `offset` | `number` | `8` | Distance from the selection's last line, in px. |

### Events

| Event | Arguments | Description |
|------|------|-------------|
| `action` | `({ id, action, selection })` | A preset action was pressed; carries the selection snapshot. |
| `submit` | `({ prompt, selection })` | Enter or send was pressed; `prompt` is trimmed, and a blank one never fires. |
| `keep` / `discard` | `()` | Keep or discard in the result state. |
| `retry` | `()` | Retry in the result state. |
| `update:expanded` | `(expanded: boolean)` | The folded group opened or closed. |
| `update:prompt` | `(prompt: string)` | The field's content changed. |

### Slots

| Slot | Scope | Description |
|------|------|-------------|
| `action-icon` | `{ action }` | Replaces an action's glyph; required for a custom `id`. |
| `busy` | `{ label }` | Replaces the busy readout. |
| `result` | — | Replaces the keep / discard / retry cluster. |

### Exposed Methods

| Method | Description |
|------|-------------|
| `updatePosition()` | Repositions against the current `selection.rects`; a streaming host must call it. |
| `focusInput()` | Focuses the free-text field. |
| `el` | The bar's root element, for `useSelectionAnchor`'s `ignore`. |

### useSelectionAnchor Options

| Option | Type | Default | Description |
|------|------|---------|-------------|
| `root` | `MaybeRefOrGetter<Element \| null \| undefined>` | — | Confines tracking to one subtree; omit to watch the whole document. |
| `debounce` | `number` | `120` | Settle time for `selectionchange`, in ms. |
| `minLength` | `number` | `1` | Minimum trimmed length. |
| `disabled` | `MaybeRefOrGetter<boolean>` | `false` | Stops reporting without unmounting. |
| `ignore` | `MaybeRefOrGetter<Array<Element \| null \| undefined>>` | `[]` | Focus inside these elements doesn't count as a deselection. |

Returns `{ selection, clear }`: `selection` is a `Ref<SelectionPayload | null>`, and `clear()` is called once the rewrite is applied or dropped. `resolveSelectionPayload()` is its pure internal rule, exported for testing or custom tracking. Both are runtime functions; import them explicitly from `@talex-touch/tuffex/selection-actions`.

## Overview

- The anchor is the bottom of the selection's last line, centered on the whole selection. The bar never flips (`disableFlip`); it is only shifted back into the viewport horizontally.
- The anchor is a virtual reference, so `floating-ui` won't follow text reflow; the host calls `updatePosition()` while streaming, and the component handles resize and scroll.
- `selection` is a snapshot: focusing the field clears the browser selection, but actions still target the payload passed in.
- The root swallows `pointerdown`, so pressing a button neither moves focus nor destroys the selection, except inside an `input`, `textarea`, or `contenteditable`. Safari and Firefox differ in caret behavior afterward; verify on real browsers.
- The root is `role="group"`, leaving arrow keys to the field's caret; folded actions, and the send control while `prompt` is empty, stay out of the tab order.
- The entrance is a spring that overshoots its end value; under reduced motion it is dropped, the width tween is skipped, and the bar rests on its visible end state.

## Technologies

- The width morph runs through the Web Animations API, so the component reads `prefers-reduced-motion` in script; tracking builds on `useTextSelection` from `@vueuse/core` and clones the selection's range.
- Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT; upstream has no real selection, so the tracking layer is new to this port.
- Source: `packages/tuffex/packages/components/src/selection-actions/`.

<TuffDocSourceLink />
