Editable

Edit text in place with save and cancel.

Project details

Click text to edit. Enter saves and Esc cancels; use Ctrl / ⌘ + Enter for multiline text.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Card, Editable, FormField, Stack, Text } from '@hina-ui/vue'

  const title = ref('Hina UI library')
  const description = ref('Consistent, restrained interactions for content sites and admin tools.')
</script>

<template>
  <Card class="w-full max-w-lg">
    <Stack gap="lg">
      <Text weight="medium">Project details</Text>
      <FormField label="Project name">
        <Editable v-model="title" :maxlength="60" />
      </FormField>
      <FormField label="Description">
        <Editable v-model="description" multiline :rows="3" />
      </FormField>
      <Text size="sm" tone="muted">
        Click text to edit. Enter saves and Esc cancels; use Ctrl / ⌘ + Enter for multiline text.
      </Text>
    </Stack>
  </Card>
</template>
vue

Usage

import { Editable } from '@hina-ui/vue'
ts

For names, titles and notes that normally appear as text. v-model holds the committed value; typing changes an internal draft until you save. Cancel restores the committed text. Use Input or Textarea for fields that remain visible and synchronize every keystroke.

Click to edit. The input receives focus and selects its text. Enter or focus leaving the whole component saves; Esc cancels. Moving focus between the input and its controls does not submit, and clicking Cancel never triggers a blur save first.

Saved: Untitled document

<script setup lang="ts">
  import { ref } from 'vue'
  import { Editable, Stack, Text } from '@hina-ui/vue'

  const title = ref('Untitled document')
</script>

<template>
  <Stack class="w-full max-w-sm">
    <Editable v-model="title" aria-label="Document name" />
    <Text size="sm" tone="muted">Saved: {{ title || '—' }}</Text>
  </Stack>
</template>
vue

Examples

Activation and submission

activationMode supports click, double click and manual activation. Click and double-click modes both accept Enter or Space when focused; Tab alone does not start editing. Manual mode uses the edit button, v-model:editing, or the exposed edit() method.

submitMode determines keyboard and blur submission. The explicit save control always works:

ValueBehavior
bothSave on Enter or focus leaving the component; default
enterSave on Enter; preserve editing and the draft on blur
blurSave on outside focus; Enter does not submit
manualOnly save explicitly; Enter and blur preserve the draft

The first example uses multiline for its description: Enter inserts a newline; Ctrl / ⌘ + Enter follows the Enter rule above. IME composition does not trigger save or cancel.

Multiline editing uses Textarea. Content beyond rows scrolls inside ScrollArea, keeping the caret visible while typing.

Press Enter or Space when focused to edit with a keyboard.

Use the edit button to make changes

The draft stays open until you save or cancel.

<script setup lang="ts">
  import { reactive } from 'vue'
  import { Editable, FormField, Stack } from '@hina-ui/vue'

  const values = reactive({
    double: 'Double-click to rename this document',
    manual: 'Use the edit button to make changes',
  })
</script>

<template>
  <Stack class="w-full max-w-sm" gap="lg">
    <FormField
      label="Double click"
      description="Press Enter or Space when focused to edit with a keyboard."
    >
      <Editable v-model="values.double" activation-mode="dblclick" />
    </FormField>
    <FormField
      label="Manual activation"
      description="The draft stays open until you save or cancel."
    >
      <Editable v-model="values.manual" activation-mode="manual" submit-mode="manual" />
    </FormField>
  </Stack>
</template>
vue

Async saving and retry

Pass a function using :on-save="save". It receives the new and previous values and may return a Promise. The model updates and submit fires only after success. Unchanged values exit editing without another save.

While saving, the draft and input stay visible, a loading indicator appears, and repeated submission and cancellation are blocked. Throwing an Error displays its message; other failures use the localized fallback. The error event receives the original error. Failure preserves the draft for retry or cancellation; editing clears the old error. Use onSave for requests the component must await, rather than an async @submit listener: submit is a success notification.

External model updates, closing editing, disabling the control, making it read-only, or unmounting invalidate outstanding results. They cannot overwrite a newer value. Network cancellation remains the caller’s responsibility.

Stay in edit mode while saving. A failed save keeps the draft for retry.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Editable, FormField, Stack, Switch, Text } from '@hina-ui/vue'

  const name = ref('Creator workspace')
  const fail = ref(false)
  const saved = ref(false)

  async function save(value: string) {
    saved.value = false
    if (!value.trim()) throw new Error('Enter a workspace name')
    await new Promise(resolve => setTimeout(resolve, 800))
    if (fail.value) throw new Error('Could not save. Disable the simulated failure and try again.')
    saved.value = true
  }
</script>

<template>
  <Stack class="w-full max-w-sm">
    <FormField
      label="Workspace name"
      description="Stay in edit mode while saving. A failed save keeps the draft for retry."
    >
      <Editable v-model="name" :on-save="save" submit-mode="enter" />
    </FormField>
    <FormField label="Simulate a failed save" orientation="horizontal">
      <Switch v-model="fail" />
    </FormField>
    <Text v-if="saved" role="status" size="sm" tone="muted">Workspace name saved.</Text>
  </Stack>
</template>
vue

Custom preview and controls

#preview changes how the value is displayed while keeping activation and focus management. In click and double-click modes the preview is a button, so do not nest interactive controls or links inside it. #actions provides state and methods for a custom action area.

Set :controls="false" to hide built-in controls. With manual activation or submission, supply the corresponding actions through slots or exposed methods.

v1.8.0
<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Editable, FormField, Stack, Tag } from '@hina-ui/vue'

  const version = ref('1.8.0')
</script>

<template>
  <Stack class="w-full max-w-sm">
    <FormField label="Release version">
      <Editable v-model="version" activation-mode="manual" submit-mode="manual">
        <template #preview="{ value }">
          <Tag tone="info">v{{ value }}</Tag>
        </template>
        <template #actions="{ editing, saving, edit, submit, cancel }">
          <template v-if="editing">
            <Button size="sm" :loading="saving" @click="submit">Save version</Button>
            <Button size="sm" variant="ghost" tone="neutral" :disabled="saving" @click="cancel">
              Cancel
            </Button>
          </template>
          <Button v-else size="sm" variant="outline" tone="neutral" @click="edit">
            Edit version
          </Button>
        </template>
      </Editable>
    </FormField>
  </Stack>
</template>
vue

Sizes and states

The three sizes share Input’s typography, height and padding. Single-line preview and input have equal width and text alignment; controls appear underneath without squeezing the text. Read-only content can be selected and copied. Disabled content cannot enter editing.

Read-only project name

Text can still be selected and copied.

<script setup lang="ts">
  import { Editable, FormField, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-sm">
    <Editable size="sm" model-value="Small text" aria-label="Small" />
    <Editable size="md" model-value="Default text" aria-label="Default" />
    <Editable size="lg" model-value="Large text" aria-label="Large" />
    <FormField label="Read only" description="Text can still be selected and copied.">
      <Editable model-value="Read-only project name" readonly />
    </FormField>
    <FormField label="Disabled">
      <Editable model-value="Editing unavailable" disabled />
    </FormField>
    <FormField label="Empty value">
      <Editable placeholder="Add a note…" />
    </FormField>
  </Stack>
</template>
vue

In a form

FormField connects labels, descriptions, errors and disabled state. Form validates committed values. This example uses v-model:editing to disable submission of the whole form until the field edit is confirmed.

required and maxlength constrain the editor itself. Use Form rules or throw a validation error from onSave for more complex rules. name includes the committed value in native FormData.

Confirm the field edit before saving the profile.

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Editable, Form, FormField, Text } from '@hina-ui/vue'

  const values = reactive({ displayName: '' })
  const editing = ref(false)
  const saved = ref('')
  const schema = v.object({
    displayName: v.pipe(v.string(), v.trim(), v.minLength(2, 'Use at least 2 characters')),
  })

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = (data as { displayName: string }).displayName
  }
</script>

<template>
  <Form
    v-slot="{ submitting }"
    :values="values"
    :rules="schema"
    class="w-full max-w-sm"
    @submit="save"
  >
    <FormField
      name="displayName"
      label="Display name"
      description="Confirm the field edit before saving the profile."
      required
    >
      <Editable
        v-model="values.displayName"
        v-model:editing="editing"
        name="displayName"
        placeholder="Enter a display name"
      />
    </FormField>
    <Button type="submit" :loading="submitting" :disabled="editing" class="self-start">
      Save profile
    </Button>
    <Text v-if="saved" role="status" size="sm" tone="muted">Saved: {{ saved }}</Text>
  </Form>
</template>
vue

Keyboard and focus

  • Preview: Tab focuses; Enter or Space starts editing. Manual mode uses the edit control.
  • Single-line input: Enter follows submitMode without submitting an outer form.
  • Multiline input: Enter inserts a newline; Ctrl / ⌘ + Enter follows submitMode.
  • Esc cancels the draft without closing a surrounding Dialog. Composition and pending saves block cancellation.
  • Saving and cancellation return focus to the preview if focus is still inside the component. They do not take focus back from another control.
  • Initial editing=true renders the editor during SSR without grabbing focus on mount. Later transitions into editing focus it automatically.

API

Props

Prop
Type
Default
Description
modelValue
string
''
Committed text; supports v-model
editing
boolean
false
Edit state; supports v-model:editing
activationMode
'click' | 'dblclick' | 'manual'
'click'
How to begin editing
submitMode
'enter' | 'blur' | 'both' | 'manual'
'both'
Keyboard and blur submission
selectOnFocus
boolean
true
Select all text on entering edit mode
multiline
boolean
false
Use a textarea
rows
number
3
Initial textarea rows
controls
boolean
true
Show default actions
onSave
EditableSave
—
Awaited callback before updating the model
placeholder
string
Localized text
Empty preview and input placeholder
name
string
—
Native form field name for the committed value
required
boolean
false
Prevent committing an empty value
maxlength
number
—
Input length limit
size
'sm' | 'md' | 'lg'
'md'
Size
disabled
boolean
false
Disable editing
readonly
boolean
false
Read-only text; allows selection and copying
invalid
boolean
false
Mark the field invalid
class
string
—
Root classes

Other attributes such as id, aria-label, and autocomplete go to the active preview or input. class styles the root; style styles the active preview or input.

Slots

SlotScope
Description
preview{ value: string, empty: boolean }
Display the committed value
actionsEditableControls
Replace default controls

Events

Event
Payload
Description
update:modelValue
string
Successfully committed value
update:editing
boolean
Editing changed
edit
—
Entered edit mode
submit
value: string, previousValue: string
Saved a change successfully
cancel
draft: string
Discarded draft
error
unknown
Save callback failed

Exposed methods and types

Instances expose edit(), submit(): Promise<boolean>, cancel(), focus(), and the current input, draft, saving, and error. input exists only while editing on the client. submit() returns whether editing completed successfully. cancel() has no effect during saving.

type EditableSave = (value: string, previousValue: string) => void | Promise<void>

interface EditableControls {
  editing: boolean
  draft: string
  dirty: boolean
  saving: boolean
  disabled: boolean
  error: string
  edit: () => void
  submit: () => Promise<boolean>
  cancel: () => void
}
ts