shshadcn-htmx

Components

Edit In Place

A read-only record with an Edit affordance that swaps in a pre-filled form. Save issues a PUT; Cancel re-fetches the view. The canonical htmx editable primitive — built entirely on outerHTML swaps over REST, no modal, no custom JS.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/edit-in-place.json

2. Use it

components/ui/edit-in-place.tsx
import { EditInPlace, EditInPlaceForm } from "@/components/ui/edit-in-place"

// View — GET /users/1 returns this. Edit fetches the form below.
<EditInPlace id="user" editHref="/users/1/edit" fields={[
  { label: "Name",  value: user.name },
  { label: "Email", value: user.email, type: "email" },
]} />

// Editor — GET /users/1/edit returns this. Save PUTs; Cancel re-GETs the view.
<EditInPlaceForm id="user" putHref="/users/1" cancelHref="/users/1" fields={[
  { label: "Name",  value: user.name },
  { label: "Email", value: user.email, type: "email" },
]} />
Or copy the source manually
components/ui/edit-in-place.tsx
/** @jsxImportSource hono/jsx */
import type { Child, PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
import { buttonClasses } from "@/registry/ui/button"
import { inputClasses } from "@/registry/ui/input"
import { labelClasses } from "@/registry/ui/label"

// Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4.
//
// The canonical htmx editable record: a read-only view with an "Edit"
// affordance that swaps in a pre-filled form. Save = PUT; Cancel re-GETs
// the view. No modal, no custom JS — the whole thing rides on outerHTML
// swaps over REST.
//
// Built on:
//   repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md
//     (GET /users/1 → view, GET /users/1/edit → form, PUT /users/1 → view)
//   repos/htmx/www/src/content/reference/01-attributes/08-hx-target.md:12-17
//     hx-target="this" targets the element making the request.
//   repos/htmx/www/src/content/reference/01-attributes/07-hx-swap.md:35-41
//     hx-swap="outerHTML" replaces the whole element with the response.
//   repos/htmx/www/src/content/reference/01-attributes/03-hx-put.md
//     hx-put issues the REST PUT on Save.
//
// Native semantics:
//   - The view is a <dl> description list — the right element for
//     name/value record pairs.
//     repos/mdn/files/en-us/web/html/reference/elements/dl/index.md
//   - The edit affordance is a real <button> and the editor is a real
//     <form> with native <input>s, so constraint validation, Enter-to-
//     submit, and focus all come from the platform.
//     repos/mdn/files/en-us/web/html/reference/elements/form/index.md
//
// Class strings mirror Card (rounded bordered container) + Input + Button,
// so the view and the editor occupy the same visual footprint and the swap
// looks like an in-place toggle rather than a layout jump.
//
// Style analogues: registry/ui/card.tsx, registry/ui/input.tsx,
// registry/ui/button.tsx, registry/ui/label.tsx.

// Shared container so the read-only view and the editor render at the same
// size — the outerHTML swap then reads as a true in-place toggle.
const container =
  "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm"

// One field's label, styled as the small uppercase eyebrow shadcn uses for
// record metadata.
const fieldTermClass =
  "text-xs font-medium tracking-wide text-muted-foreground uppercase"

const fieldValueClass = "mt-0.5 text-sm font-medium text-foreground"

export type EditInPlaceField = {
  // Visible label for the field, e.g. "Email".
  label: string
  // The current, read-only value shown in the view.
  value: Child
  // Form field name used by the editor's <input name>. Defaults to a
  // lowercased label, but pass it explicitly for anything non-trivial.
  name?: string
  // Input type for the editor (text, email, url, tel, …). Default "text".
  type?: string
  // Raw value passed to the editor's <input value>. Falls back to `value`
  // when that is a plain string.
  inputValue?: string
  required?: boolean
}

// ── View ────────────────────────────────────────────────────────────────
// The read-only record. Carries hx-target="this" + hx-swap="outerHTML" so
// any descendant request (the Edit button, and later the editor's Save /
// Cancel) replaces this whole element. `editHref` is the GET that returns
// the editor fragment.

type EditInPlaceProps = PropsWithChildren<{
  // GET endpoint that returns the editor form fragment.
  editHref: string
  // Fields rendered as a <dl>. Omit when you pass custom children instead.
  fields?: EditInPlaceField[]
  // Label on the Edit button. Default "Edit".
  editLabel?: string
  id?: string
  class?: ClassValue
}> &
  Record<string, any>

export function EditInPlace(props: EditInPlaceProps) {
  const {
    children,
    editHref,
    fields,
    editLabel = "Edit",
    class: className,
    id,
    ...rest
  } = props

  return (
    <div
      data-slot="edit-in-place"
      data-mode="view"
      id={id}
      hx-target="this"
      hx-swap="outerHTML"
      class={cn(container, className)}
      {...rest}
    >
      {fields ? (
        <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
          {fields.map((f) => (
            <div>
              <dt class={fieldTermClass}>{f.label}</dt>
              <dd class={fieldValueClass}>{f.value}</dd>
            </div>
          ))}
        </dl>
      ) : (
        children
      )}
      <div class="flex">
        <button
          type="button"
          data-slot="edit-in-place-edit"
          hx-get={editHref}
          hx-target="closest [data-slot='edit-in-place']"
          hx-swap="outerHTML"
          class={buttonClasses({ variant: "outline", size: "sm" })}
        >
          {editLabel}
        </button>
      </div>
    </div>
  )
}

// ── Editor ──────────────────────────────────────────────────────────────
// The pre-filled form returned by `editHref`. Submitting issues the PUT
// (Save); Cancel re-GETs the view. Both target `this` form and swap
// outerHTML, restoring the view in place.

type EditInPlaceFormProps = PropsWithChildren<{
  // PUT endpoint hit on Save.
  putHref: string
  // GET endpoint that restores the read-only view on Cancel.
  cancelHref: string
  // Fields to pre-fill. Omit when you pass custom children instead.
  fields?: EditInPlaceField[]
  saveLabel?: string
  cancelLabel?: string
  id?: string
  class?: ClassValue
}> &
  Record<string, any>

export function EditInPlaceForm(props: EditInPlaceFormProps) {
  const {
    children,
    putHref,
    cancelHref,
    fields,
    saveLabel = "Save",
    cancelLabel = "Cancel",
    class: className,
    id,
    ...rest
  } = props

  return (
    <form
      data-slot="edit-in-place"
      data-mode="edit"
      id={id}
      hx-put={putHref}
      hx-target="this"
      hx-swap="outerHTML"
      class={cn(container, className)}
      {...rest}
    >
      {fields ? (
        <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
          {fields.map((f) => {
            const name = f.name ?? f.label.toLowerCase()
            const fieldId = `${id ?? "field"}-${name}`
            const value =
              f.inputValue ?? (typeof f.value === "string" ? f.value : undefined)
            return (
              <div class="grid gap-1.5">
                <label for={fieldId} class={labelClasses({ class: fieldTermClass })}>
                  {f.label}
                </label>
                <input
                  id={fieldId}
                  name={name}
                  type={f.type ?? "text"}
                  value={value}
                  required={f.required}
                  data-slot="input"
                  class={inputClasses()}
                />
              </div>
            )
          })}
        </div>
      ) : (
        children
      )}
      <div class="flex gap-2">
        <button
          type="submit"
          data-slot="edit-in-place-save"
          class={buttonClasses({ size: "sm" })}
        >
          {saveLabel}
        </button>
        <button
          type="button"
          data-slot="edit-in-place-cancel"
          hx-get={cancelHref}
          hx-target="closest [data-slot='edit-in-place']"
          hx-swap="outerHTML"
          class={buttonClasses({ variant: "secondary", size: "sm" })}
        >
          {cancelLabel}
        </button>
      </div>
    </form>
  )
}

1. Save the file

Copy edit-in-place.html into templates/components/.

2. Use it

templates/components/edit-in-place.html
{% from "components/edit-in-place.html" import edit_in_place, edit_in_place_form %}

{# View #}
{{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
     {"label": "Name",  "value": user.name},
     {"label": "Email", "value": user.email, "type": "email"},
]) }}

{# Editor #}
{{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
     {"label": "Name",  "value": user.name},
     {"label": "Email", "value": user.email, "type": "email"},
]) }}
View source
templates/components/edit-in-place.html
{# Edit In Place macros — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/edit-in-place.tsx for Python/Flask/FastAPI/Django/Jinja2.

   The canonical htmx editable record: a read-only view with an Edit button
   that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view.
   No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
   See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.

   Usage:
       {% from "components/edit-in-place.html" import edit_in_place, edit_in_place_form %}

       {# View — GET /users/1 returns this #}
       {{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
            {"label": "Name",  "value": user.name},
            {"label": "Email", "value": user.email, "type": "email"},
       ]) }}

       {# Editor — GET /users/1/edit returns this #}
       {{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
            {"label": "Name",  "value": user.name},
            {"label": "Email", "value": user.email, "type": "email"},
       ]) }}

   hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
   request replaces the whole element in place. #}

{%- set _container = "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -%}
{%- set _term = "text-xs font-medium tracking-wide text-muted-foreground uppercase" -%}
{%- set _value = "mt-0.5 text-sm font-medium text-foreground" -%}
{%- set _edit_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _save_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _cancel_btn = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -%}
{%- set _input = "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70" -%}
{%- set _label = "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase" -%}

{# View — read-only record + Edit button. #}
{% macro edit_in_place(edit_href, fields=none, edit_label="Edit", id=none, extra_class="", caller=none) %}
<div data-slot="edit-in-place" data-mode="view"
     {%- if id %} id="{{ id }}"{% endif %}
     hx-target="this" hx-swap="outerHTML"
     class="{{ _container }} {{ extra_class }}">
  {%- if fields %}
  <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    {%- for f in fields %}
    <div>
      <dt class="{{ _term }}">{{ f.label }}</dt>
      <dd class="{{ _value }}">{{ f.value }}</dd>
    </div>
    {%- endfor %}
  </dl>
  {%- elif caller %}{{ caller() }}{% endif %}
  <div class="flex">
    <button type="button" data-slot="edit-in-place-edit" hx-get="{{ edit_href }}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{ _edit_btn }}">{{ edit_label }}</button>
  </div>
</div>
{% endmacro %}

{# Editor — pre-filled form. Save submits the PUT; Cancel re-GETs the view. #}
{% macro edit_in_place_form(put_href, cancel_href, fields=none, save_label="Save", cancel_label="Cancel", id=none, extra_class="", caller=none) %}
<form data-slot="edit-in-place" data-mode="edit"
      {%- if id %} id="{{ id }}"{% endif %}
      hx-put="{{ put_href }}" hx-target="this" hx-swap="outerHTML"
      class="{{ _container }} {{ extra_class }}">
  {%- if fields %}
  <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    {%- for f in fields %}
    {%- set name = f.name if f.name else f.label|lower %}
    {%- set field_id = (id if id else "field") ~ "-" ~ name %}
    <div class="grid gap-1.5">
      <label for="{{ field_id }}" class="{{ _label }}">{{ f.label }}</label>
      <input id="{{ field_id }}" name="{{ name }}" type="{{ f.type if f.type else 'text' }}"
             {%- if f.value is not none %} value="{{ f.value }}"{% endif %}
             {%- if f.required %} required{% endif %}
             data-slot="input" class="{{ _input }}">
    </div>
    {%- endfor %}
  </div>
  {%- elif caller %}{{ caller() }}{% endif %}
  <div class="flex gap-2">
    <button type="submit" data-slot="edit-in-place-save" class="{{ _save_btn }}">{{ save_label }}</button>
    <button type="button" data-slot="edit-in-place-cancel" hx-get="{{ cancel_href }}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{ _cancel_btn }}">{{ cancel_label }}</button>
  </div>
</form>
{% endmacro %}

1. Save the file

Add edit-in-place.tmpl alongside your templates.

2. Use it

components/edit-in-place.tmpl
{{/* View */}}
{{template "edit_in_place" (dict "ID" "user" "EditHref" "/users/1/edit"
  "Fields" $fields)}}

{{/* Editor */}}
{{template "edit_in_place_form" (dict "ID" "user"
  "PutHref" "/users/1" "CancelHref" "/users/1" "Fields" $fields)}}
View source
components/edit-in-place.tmpl
{{/*
  Edit In Place templates — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/edit-in-place.tsx for Go projects using html/template.

  The canonical htmx editable record: a read-only view with an Edit button
  that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view. No
  modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.

  Two templates:
    - "edit_in_place"      — the read-only view (GET /users/1 returns this).
    - "edit_in_place_form" — the pre-filled editor (GET /users/1/edit).

  Field struct shared by both:

      type EipField struct {
          Label    string // "Email"
          Value    string // current value (shown in view, prefilled in editor)
          Name     string // form field name (required in the editor)
          Type     string // input type (default "text")
          Required bool
      }

  View args:

      type EipArgs struct {
          ID        string
          EditHref  string       // GET endpoint returning the editor
          EditLabel string       // default "Edit"
          Fields    []EipField
      }
      tpl.ExecuteTemplate(w, "edit_in_place", EipArgs{
          ID: "user", EditHref: "/users/1/edit",
          Fields: []EipField{
              {Label: "Name", Value: "Joe Smith"},
              {Label: "Email", Value: "[email protected]", Type: "email"},
          },
      })

  Editor args:

      type EipFormArgs struct {
          ID          string
          PutHref     string     // PUT endpoint hit on Save
          CancelHref  string     // GET endpoint restoring the view on Cancel
          SaveLabel   string     // default "Save"
          CancelLabel string     // default "Cancel"
          Fields      []EipField
      }

  hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
  request replaces the whole element in place.
*/}}

{{define "edit_in_place"}}
{{- $container := "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -}}
{{- $term := "text-xs font-medium tracking-wide text-muted-foreground uppercase" -}}
{{- $value := "mt-0.5 text-sm font-medium text-foreground" -}}
{{- $editBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $editLabel := or .EditLabel "Edit" -}}
<div data-slot="edit-in-place" data-mode="view"
     {{- if .ID}} id="{{.ID}}"{{end}}
     hx-target="this" hx-swap="outerHTML"
     class="{{$container}}">
  <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    {{- range .Fields}}
    <div>
      <dt class="{{$term}}">{{.Label}}</dt>
      <dd class="{{$value}}">{{.Value}}</dd>
    </div>
    {{- end}}
  </dl>
  <div class="flex">
    <button type="button" data-slot="edit-in-place-edit" hx-get="{{.EditHref}}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{$editBtn}}">{{$editLabel}}</button>
  </div>
</div>
{{end}}

{{define "edit_in_place_form"}}
{{- $container := "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm" -}}
{{- $label := "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase" -}}
{{- $input := "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70" -}}
{{- $saveBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $cancelBtn := "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5" -}}
{{- $saveLabel := or .SaveLabel "Save" -}}
{{- $cancelLabel := or .CancelLabel "Cancel" -}}
{{- $id := or .ID "field" -}}
<form data-slot="edit-in-place" data-mode="edit"
      {{- if .ID}} id="{{.ID}}"{{end}}
      hx-put="{{.PutHref}}" hx-target="this" hx-swap="outerHTML"
      class="{{$container}}">
  <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    {{- range .Fields}}
    {{- $name := .Name}}
    {{- $fieldId := printf "%s-%s" $id $name}}
    <div class="grid gap-1.5">
      <label for="{{$fieldId}}" class="{{$label}}">{{.Label}}</label>
      <input id="{{$fieldId}}" name="{{$name}}" type="{{or .Type "text"}}"
             value="{{.Value}}"
             {{- if .Required}} required{{end}}
             data-slot="input" class="{{$input}}">
    </div>
    {{- end}}
  </div>
  <div class="flex gap-2">
    <button type="submit" data-slot="edit-in-place-save" class="{{$saveBtn}}">{{$saveLabel}}</button>
    <button type="button" data-slot="edit-in-place-cancel" hx-get="{{.CancelHref}}" hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML" class="{{$cancelBtn}}">{{$cancelLabel}}</button>
  </div>
</form>
{{end}}

1. Save the file

Drop edit_in_place.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/edit_in_place.ex
<%# View %>
<.edit_in_place id="user" edit_href={~p"/users/1/edit"} fields={[
  %{label: "Name", value: @user.name},
  %{label: "Email", value: @user.email, type: "email"}
]} />

<%# Editor %>
<.edit_in_place_form id="user" put_href={~p"/users/1"} cancel_href={~p"/users/1"} fields={[
  %{label: "Name", value: @user.name, name: "name"},
  %{label: "Email", value: @user.email, name: "email", type: "email"}
]} />
View source
lib/my_app_web/components/edit_in_place.ex
defmodule ShadcnHtmx.Components.EditInPlace do
  @moduledoc """
  Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/edit-in-place.tsx. The canonical htmx editable record:
  a read-only view with an Edit button that swaps in a pre-filled form.
  Save = PUT, Cancel re-GETs the view. No modal, no custom JS — the whole
  thing rides on outerHTML swaps over REST.
  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.

  Two function components:
    - `edit_in_place/1`      — the read-only view (GET /users/1 returns this).
    - `edit_in_place_form/1` — the pre-filled editor (GET /users/1/edit).

  Each `:fields` entry is a map:

      %{label: "Email", value: "[email protected]", name: "email", type: "email", required: true}

  ## Examples

      <%# View %>
      <.edit_in_place id="user" edit_href="/users/1/edit" fields={[
        %{label: "Name", value: @user.name},
        %{label: "Email", value: @user.email, type: "email"}
      ]} />

      <%# Editor %>
      <.edit_in_place_form id="user" put_href="/users/1" cancel_href="/users/1" fields={[
        %{label: "Name", value: @user.name},
        %{label: "Email", value: @user.email, type: "email"}
      ]} />

  hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
  request replaces the whole element in place.
  """

  use Phoenix.Component

  @container "flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm"
  @term "text-xs font-medium tracking-wide text-muted-foreground uppercase"
  @value "mt-0.5 text-sm font-medium text-foreground"
  @label "flex items-center gap-2 text-sm leading-none font-medium select-none " <>
            "group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 " <>
            "peer-disabled:cursor-not-allowed peer-disabled:opacity-50 " <>
            "text-xs font-medium tracking-wide text-muted-foreground uppercase"
  @input "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs " <>
            "transition-[color,box-shadow] outline-none " <>
            "selection:bg-primary selection:text-primary-foreground " <>
            "file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground " <>
            "placeholder:text-muted-foreground " <>
            "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " <>
            "md:text-sm dark:bg-input/30 " <>
            "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
            "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
            "[&.htmx-request]:opacity-70"
  @btn_base "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none " <>
              "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
              "disabled:pointer-events-none disabled:opacity-50 " <>
              "aria-disabled:pointer-events-none aria-disabled:opacity-50 " <>
              "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
              "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 " <>
              "[&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 " <>
              "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5"
  @edit_btn @btn_base <>
              " border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
  @save_btn @btn_base <> " bg-primary text-primary-foreground hover:bg-primary/90"
  @cancel_btn @btn_base <> " bg-secondary text-secondary-foreground hover:bg-secondary/80"

  attr :id, :string, default: nil
  attr :edit_href, :string, required: true
  attr :edit_label, :string, default: "Edit"
  attr :fields, :list, default: []
  attr :class, :string, default: nil
  attr :rest, :global, include: ~w(hx-get hx-post hx-put hx-patch hx-target hx-swap hx-trigger hx-indicator)

  def edit_in_place(assigns) do
    assigns =
      assigns
      |> assign(:container, @container)
      |> assign(:term, @term)
      |> assign(:value, @value)
      |> assign(:edit_btn, @edit_btn)

    ~H"""
    <div
      data-slot="edit-in-place"
      data-mode="view"
      id={@id}
      hx-target="this"
      hx-swap="outerHTML"
      class={[@container, @class]}
      {@rest}
    >
      <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
        <div :for={f <- @fields}>
          <dt class={@term}>{f.label}</dt>
          <dd class={@value}>{f.value}</dd>
        </div>
      </dl>
      <div class="flex">
        <button
          type="button"
          data-slot="edit-in-place-edit"
          hx-get={@edit_href}
          hx-target="closest [data-slot='edit-in-place']"
          hx-swap="outerHTML"
          class={@edit_btn}
        >
          {@edit_label}
        </button>
      </div>
    </div>
    """
  end

  attr :id, :string, default: nil
  attr :put_href, :string, required: true
  attr :cancel_href, :string, required: true
  attr :save_label, :string, default: "Save"
  attr :cancel_label, :string, default: "Cancel"
  attr :fields, :list, default: []
  attr :class, :string, default: nil
  attr :rest, :global, include: ~w(hx-target hx-swap hx-trigger hx-indicator)

  def edit_in_place_form(assigns) do
    base_id = assigns.id || "field"

    assigns =
      assigns
      |> assign(:base_id, base_id)
      |> assign(:container, @container)
      |> assign(:label, @label)
      |> assign(:input, @input)
      |> assign(:save_btn, @save_btn)
      |> assign(:cancel_btn, @cancel_btn)

    ~H"""
    <form
      data-slot="edit-in-place"
      data-mode="edit"
      id={@id}
      hx-put={@put_href}
      hx-target="this"
      hx-swap="outerHTML"
      class={[@container, @class]}
      {@rest}
    >
      <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
        <div :for={f <- @fields} class="grid gap-1.5">
          <label for={"#{@base_id}-#{f[:name] || String.downcase(f.label)}"} class={@label}>
            {f.label}
          </label>
          <input
            id={"#{@base_id}-#{f[:name] || String.downcase(f.label)}"}
            name={f[:name] || String.downcase(f.label)}
            type={f[:type] || "text"}
            value={f.value}
            required={f[:required] || nil}
            data-slot="input"
            class={@input}
          />
        </div>
      </div>
      <div class="flex gap-2">
        <button type="submit" data-slot="edit-in-place-save" class={@save_btn}>
          {@save_label}
        </button>
        <button
          type="button"
          data-slot="edit-in-place-cancel"
          hx-get={@cancel_href}
          hx-target="closest [data-slot='edit-in-place']"
          hx-swap="outerHTML"
          class={@cancel_btn}
        >
          {@cancel_label}
        </button>
      </div>
    </form>
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens.

2. Use it

snippets/edit-in-place.html
<!-- View: GET /users/1 returns this -->
<div data-slot="edit-in-place" id="user" hx-target="this" hx-swap="outerHTML" class="…">
  <dl>…</dl>
  <button type="button" hx-get="/users/1/edit"
          hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
          class="…">Edit</button>
</div>

<!-- Editor: GET /users/1/edit returns this -->
<form data-slot="edit-in-place" hx-put="/users/1" hx-target="this" hx-swap="outerHTML" class="…">
  <label for="user-name">Name</label>
  <input id="user-name" name="name" value="Joe Smith" class="…">
  <button type="submit">Save</button>
  <button type="button" hx-get="/users/1"
          hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML">Cancel</button>
</form>
View source
snippets/edit-in-place.html
<!--
  shadcn-htmx — raw HTML Edit In Place snippet.

  Mirrors registry/ui/edit-in-place.tsx. Drop onto any page that loads
  htmx v4 + Tailwind CSS v4 and the shadcn theme variables (card, border,
  ring, primary, secondary, muted, foreground). Relies only on theme tokens.

  The canonical htmx editable record: a read-only VIEW with an Edit button
  that GETs the EDITOR form; Save issues the PUT; Cancel re-GETs the view.
  No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.
  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.

  REST endpoints (the URL is the resource, the method is the action):
    GET  /users/1        → the view fragment below
    GET  /users/1/edit   → the editor fragment below
    PUT  /users/1        → updates, then returns the view fragment

  hx-target="this" + hx-swap="outerHTML" on each root mean every descendant
  request (Edit / Save / Cancel) replaces the whole element in place.
-->

<!-- ─── VIEW (GET /users/1) ─────────────────────────────────────────── -->
<div data-slot="edit-in-place" data-mode="view" id="user"
     hx-target="this" hx-swap="outerHTML"
     class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
  <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    <div>
      <dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</dt>
      <dd class="mt-0.5 text-sm font-medium text-foreground">Joe Smith</dd>
    </div>
    <div>
      <dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</dt>
      <dd class="mt-0.5 text-sm font-medium text-foreground">[email protected]</dd>
    </div>
  </dl>
  <div class="flex">
    <button type="button" data-slot="edit-in-place-edit" hx-get="/users/1/edit"
            hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
            class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Edit</button>
  </div>
</div>

<!-- ─── EDITOR (GET /users/1/edit) ──────────────────────────────────── -->
<form data-slot="edit-in-place" data-mode="edit" id="user"
      hx-put="/users/1" hx-target="this" hx-swap="outerHTML"
      class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
  <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    <div class="grid gap-1.5">
      <label for="user-name" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</label>
      <input id="user-name" name="name" type="text" value="Joe Smith" data-slot="input"
             class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70">
    </div>
    <div class="grid gap-1.5">
      <label for="user-email" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</label>
      <input id="user-email" name="email" type="email" value="[email protected]" data-slot="input"
             class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70">
    </div>
  </div>
  <div class="flex gap-2">
    <button type="submit" data-slot="edit-in-place-save"
            class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Save</button>
    <button type="button" data-slot="edit-in-place-cancel" hx-get="/users/1"
            hx-target="closest [data-slot='edit-in-place']" hx-swap="outerHTML"
            class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5">Cancel</button>
  </div>
</form>

Examples

Record view ↔ editor

Click Edit to swap in the form. Save PUTs the changes and the server returns the updated view; Cancel re-fetches the view, discarding edits.

The view carries hx-target="this" + hx-swap="outerHTML", so the Edit button's response replaces the whole card in place. The editor is a real <form>: Enter submits, the browser validates, and Save issues the PUT. No JavaScript of our own — the platform and htmx do all the work.

Name
Joe Smith
<EditInPlace id="user" editHref="/users/1/edit" fields={[
  { label: "Name",  value: user.name },
  { label: "Email", value: user.email, type: "email" },
]} />
{{ edit_in_place(edit_href="/users/1/edit", id="user", fields=[
     {"label": "Name",  "value": user.name},
     {"label": "Email", "value": user.email, "type": "email"},
]) }}
{{template "edit_in_place" (dict "ID" "user"
  "EditHref" "/users/1/edit" "Fields" $fields)}}
<.edit_in_place id="user" edit_href={~p"/users/1/edit"} fields={[
  %{label: "Name", value: @user.name},
  %{label: "Email", value: @user.email, type: "email"}
]} />
<div data-slot="edit-in-place" data-mode="view" id="ex-eip-user" hx-target="this" hx-swap="outerHTML" class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
  <dl class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    <div>
      <dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</dt>
      <dd class="mt-0.5 text-sm font-medium text-foreground">Joe Smith</dd>
    </div>
    <div>
      <dt class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</dt>
      <dd class="mt-0.5 text-sm font-medium text-foreground">[email protected]</dd>
    </div>
  </dl>
  <div class="flex">
    <button type="button" data-slot="edit-in-place-edit" hx-get="/edit-in-place/user/edit" hx-target="closest [data-slot=&#39;edit-in-place&#39;]" hx-swap="outerHTML" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 [&amp;_svg:not([class*=&#39;size-&#39;])]:size-4 [&amp;.htmx-request]:pointer-events-none [&amp;.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[&gt;svg]:px-2.5">Edit</button>
  </div>
</div>

Editor with a required field

The editor is a native form, so required + type="email" are enforced by the browser before the PUT ever fires.

Because Save is a type="submit" button inside a real <form>, native constraint validation runs first: an empty required field or a malformed email blocks submission with the browser's own message, and htmx never sends the request. Zero extra code.

<EditInPlaceForm id="user" putHref="/users/1" cancelHref="/users/1" fields={[
  { label: "Name",  value: user.name, required: true },
  { label: "Email", value: user.email, type: "email", required: true },
]} />
{{ edit_in_place_form(put_href="/users/1", cancel_href="/users/1", id="user", fields=[
     {"label": "Name",  "value": user.name, "required": true},
     {"label": "Email", "value": user.email, "type": "email", "required": true},
]) }}
{{template "edit_in_place_form" (dict "ID" "user"
  "PutHref" "/users/1" "CancelHref" "/users/1" "Fields" $fields)}}
<.edit_in_place_form id="user" put_href={~p"/users/1"} cancel_href={~p"/users/1"} fields={[
  %{label: "Name", value: @user.name, name: "name", required: true},
  %{label: "Email", value: @user.email, name: "email", type: "email", required: true}
]} />
<form data-slot="edit-in-place" data-mode="edit" id="ex-eip-edit" hx-put="/edit-in-place/user" hx-target="this" hx-swap="outerHTML" class="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm">
  <div class="flex flex-col gap-3" data-slot="edit-in-place-fields">
    <div class="grid gap-1.5">
      <label for="ex-eip-edit-name" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Name</label>
      <input id="ex-eip-edit-name" name="name" type="text" value="Joe Smith" required="" data-slot="input" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70"/>
    </div>
    <div class="grid gap-1.5">
      <label for="ex-eip-edit-email" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase">Email</label>
      <input id="ex-eip-edit-email" name="email" type="email" value="[email protected]" required="" data-slot="input" class="flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;.htmx-request]:opacity-70"/>
    </div>
  </div>
  <div class="flex gap-2">
    <button type="submit" data-slot="edit-in-place-save" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 [&amp;_svg:not([class*=&#39;size-&#39;])]:size-4 [&amp;.htmx-request]:pointer-events-none [&amp;.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[&gt;svg]:px-2.5">Save</button>
    <button type="button" data-slot="edit-in-place-cancel" hx-get="/edit-in-place/user" hx-target="closest [data-slot=&#39;edit-in-place&#39;]" hx-swap="outerHTML" class="inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 [&amp;_svg:not([class*=&#39;size-&#39;])]:size-4 [&amp;.htmx-request]:pointer-events-none [&amp;.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[&gt;svg]:px-2.5">Cancel</button>
  </div>
</form>

API Reference

Edit In Place

PropTypeDefaultDescription
editHref*string
GET endpoint that returns the editor form fragment. Set on EditInPlace (the view); wired to the Edit button's hx-get.htmxhx-get
putHref*string
PUT endpoint hit when Save is pressed. Set on EditInPlaceForm (the editor); the response (the updated view) replaces the form in place.htmxhx-put
cancelHref*string
GET endpoint that restores the read-only view on Cancel, discarding edits. Set on EditInPlaceForm.
fieldsEditInPlaceField[]
Record fields: { label, value, name?, type?, inputValue?, required? }. Rendered as a <dl> of term/value pairs in the view and as pre-filled native inputs in the editor. Omit to pass custom children instead.MDN<dl> element
editLabelstring"Edit"
Text on the Edit button in the view.
saveLabelstring"Save"
Text on the Save submit button in the editor.
cancelLabelstring"Cancel"
Text on the Cancel button in the editor.
idstring
Root id. Reused across the view<->editor swap so the element keeps its identity; also seeds each editor input id as {id}-{name}.
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference

* required