component

Editable

Inline click-to-edit text — a task title, a project name, a description. At rest it reads as the surrounding text: it inherits the context’s typography (an <h2>, a table cell, a list row) and only tints on hover. Click it and the text becomes editable in place — the caret lands where you clicked and nothing shifts, because the edit field is laid out by exactly the same rules as the text it replaces. Enter or blur saves, Escape cancels.

Title

The headline case: an Editable inside a heading takes the heading’s size and weight. Long titles wrap across lines, and keep wrapping identically while you edit.

Migrate the billing service to the new queue

---
// GENERATED from packages/demos/editable/title.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import { css } from "@pindoba/styled-system/css";
---

<h2 class={css({ fontSize: "2xl", fontWeight: "semibold", margin: 0 })}>
  <Editable
    defaultValue="Migrate the billing service to the new queue"
    label="Task title"
  />
</h2>

Multiline

multiline allows line breaks: Enter inserts a newline and ⌘/Ctrl + Enter saves. Single-line editables still wrap long text visually, but strip newlines (a pasted block of lines joins with spaces).

Move the invoice workers off the legacy cron jobs. Keep the retry policy and the dead-letter queue.
---
// GENERATED from packages/demos/editable/description.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
---

<Editable
  multiline
  label="Description"
  placeholder="Add a description…"
  defaultValue="Move the invoice workers off the legacy cron jobs.
Keep the retry policy and the dead-letter queue."
/>

Alignment

The hover tint and the edit border need a little room around the text. By default the editable takes that room outside its text (bleed): the box reaches past the text by its padding, so the text itself lines up with whatever sits above and below it — a title with its meta row, a value with its column. Leave a little space around it for the box, and turn bleed off where there is none (an edge-to-edge row, an overflow: hidden cell): the padding then sits inside and the text is inset by it.

Editing is seamless by default: no border or fill appears, the text just takes a caret, so a title stays a title while you type. In form-like rows — a settings grid, a comment composer — use editAppearance="field" to show an input box while editing. An invalid editable shows its red border either way.

Placeholder

An empty value shows the placeholder in the muted text color, both at rest and while editing. Saving an empty (or whitespace-only) draft reverts to the previous value — pass revertOnEmpty={false} to allow clearing it.

Untitled task
---
// GENERATED from packages/demos/editable/placeholder.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
---

<Editable label="Task title" placeholder="Untitled task" />

Controls

controls adds a pencil button beside the text (shown on hover and focus) and save / cancel buttons while editing. Their accessible names are prefixed with label (“Edit Project name”); translate them with labels. A disabled or readOnly editable shows no controls — there is nothing to edit.

Northwind portal
---
// GENERATED from packages/demos/editable/controls.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
---

<Editable controls label="Project name" defaultValue="Northwind portal" />

Activation

activationMode sets what enters edit mode from the text: click (default), dblclick for dense UIs where a single click selects, focus to start editing as soon as the text is tabbed to, or none so only the edit button (or the API) does. With every mode but none, Enter or Space on the focused text starts editing too. Text the user selected first — the word a double-click picks, or a dragged range — carries into the field (with the default selectOnEdit="caret"; an explicit all or end wins). Outside inline mode the editable takes the whole line, a flex row included, so a click anywhere across a list row’s width activates it. submitMode sets what saves: both (default), enter, blur or none — when blur doesn’t save, it cancels.

Double clickDouble-click to rename
FocusTab here to start editing
Edit button onlyUse the pencil to edit
---
// GENERATED from packages/demos/editable/activation.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const label = css({ color: "neutral.text.muted", fontSize: "sm" });
---

<div class={stack({ gap: "md" })}>
  <div>
    <span class={label}>Double click</span>
    <Editable
      activationMode="dblclick"
      label="Double click"
      defaultValue="Double-click to rename"
    />
  </div>
  <div>
    <span class={label}>Focus</span>
    <Editable
      activationMode="focus"
      label="Focus"
      defaultValue="Tab here to start editing"
    />
  </div>
  <div>
    <span class={label}>Edit button only</span>
    <Editable
      activationMode="none"
      controls
      label="Edit button only"
      defaultValue="Use the pencil to edit"
    />
  </div>
</div>

Controlled

Pass value and update it from onValueCommit, which fires once per save that changes the value. onValueChange reports every keystroke of the draft, onValueRevert a cancel, and onEditChange the edit mode (with why it ended: enter, blur, button, escape). In Astro the same moments are editable:change, editable:commit, editable:revert and editable:editchange events on the root.

Quarterly planning

Saved “Quarterly planning” · 0 saves

---
// GENERATED from packages/demos/editable/controlled.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const title = "Quarterly planning";
const saves = 0;
---

<div class={stack({ gap: "sm" })} data-ir-root="editable/controlled">
  <Editable label="Board name" value={title} data-ir="1" />
  <p class={css({ color: "neutral.text.muted", fontSize: "sm" })}>
    Saved “<!--ir:2-->
    {title}
    <!--/ir-->” · <!--ir:3-->
    {saves}
    <!--/ir--> saves
  </p>
</div>

<script>
  import { hydrate } from "./_client/controlled.ts";

  document
    .querySelectorAll('[data-ir-root="editable/controlled"]')
    .forEach((root) => hydrate([root]));
</script>

States

disabled dims the text and takes it out of the tab order; readOnly shows the value without allowing edits; invalid turns the edit border red and sets aria-invalid.

DisabledRead-onlyName already taken
---
// GENERATED from packages/demos/editable/states.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm" })}>
  <Editable disabled label="Disabled" defaultValue="Disabled" />
  <Editable readOnly label="Read-only" defaultValue="Read-only" />
  <Editable invalid label="Invalid" defaultValue="Name already taken" />
</div>

Forms

With name, a hidden input carries the committed value, so the editable submits with its form. Keep blur saving here (the default submitMode): with enter or none, clicking the form’s submit button blurs the field first, which cancels the edit just before the form reads it.

Release notes
---
// GENERATED from packages/demos/editable/form.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const submitted = "";
---

<form
  class={stack({ gap: "sm", align: "start" })}
  data-ir-root="editable/form"
  data-ir="1"
>
  <Editable name="title" label="Title" defaultValue="Release notes" />
  <Button type="submit">Submit</Button>
  <!--ir:2-->
  {submitted && (
    <p class={css({ color: "neutral.text.muted", fontSize: "sm" })}>
      Submitted title: <!--ir:3-->
      {submitted}
      <!--/ir-->
    </p>
  )}
  <!--/ir-->
</form>

<script>
  import { hydrate } from "./_client/form.ts";

  document
    .querySelectorAll('[data-ir-root="editable/form"]')
    .forEach((root) => hydrate([root]));
</script>

In rows

Inside a row that paints its own hover (a list, a tree, a table), drop the editable’s tint with background="none" and use activationMode="dblclick" so a single click stays a selection.

  • Design review
  • API contract
  • Launch checklist
---
// GENERATED from packages/demos/editable/list.demo.tsx — edit the source, not this file.
import Editable from "@pindoba/astro-editable";
import { css } from "@pindoba/styled-system/css";

const rows = [
  { id: "design", name: "Design review" },
  { id: "api", name: "API contract" },
  { id: "launch", name: "Launch checklist" },
];
const list = css({
  display: "flex",
  flexDirection: "column",
  gap: "3xs",
  listStyle: "none",
  margin: 0,
  padding: 0,
  maxWidth: "320px",
});
const row = css({
  borderRadius: "sm",
  _hover: { background: "neutral.surface.base" },
});
---

<ul class={list}>
  {rows.map((item) => (
    <li class={row}>
      <Editable
        background="none"
        activationMode="dblclick"
        label="Section name"
        defaultValue={item.name}
      />
    </li>
  ))}
</ul>

Keyboard

KeyWhereAction
Enter / SpacetextStart editing (caret at the end)
EnterfieldSave (single-line); newline (multiline)
⌘/Ctrl + EnterfieldSave (multiline)
EscapefieldCancel and restore the value
TabfieldLeave the field — saves (submitMode permitting)

Accessibility

  • At rest the text is a role="button" in the tab order, named "<label>: <value>". While editing it stays in place as an aria-hidden sizer and the <textarea> takes focus, named by label (aria-multiline when multiline).
  • Focus returns to the text after saving or cancelling with the keyboard or the buttons; a blur save leaves focus where the user moved it.
  • Enter is ignored while an IME composition is active, so confirming a candidate never saves early.
  • Components
  • Blocks