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>
Usage
import { Editable } from '@hina-ui/vue'
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>
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:
| Value | Behavior |
|---|---|
both | Save on Enter or focus leaving the component; default |
enter | Save on Enter; preserve editing and the draft on blur |
blur | Save on outside focus; Enter does not submit |
manual | Only 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.
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>
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>
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.
<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>
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.
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>
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.
<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>
Keyboard and focus
- Preview: Tab focuses; Enter or Space starts editing. Manual mode uses the edit control.
- Single-line input: Enter follows
submitModewithout 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=truerenders 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
| Slot | Scope | Description |
|---|---|---|
preview | { value: string, empty: boolean } | Display the committed value |
actions | EditableControls | 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
}