Skip to content
ShineUI
Install

Inplace

Click, edit, done.

<sui-inplace>MiscStable

About

Click-to-edit text. The value shows with a pencil mark; a click swaps in an input, Enter or blur saves, and Escape reverts to the last saved value. Both states take the host's font and share one box, so a title can be an inplace and nothing moves when editing starts. Use to rename in place: memos, report titles, account names.

Usage

<sui-inplace label="Memo" value="Fall supplies order. Deliver to dock."></sui-inplace> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-inplace"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiInplace } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiInplace label="Memo" value="Fall supplies order. Deliver to dock." onSuiClose={(e) => console.log(e.detail)} />; }
<script setup> import { SuiInplace } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiInplace label="Memo" value="Fall supplies order. Deliver to dock." @sui-close="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiInplace } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiInplace], template: `<sui-inplace label="Memo" value="Fall supplies order. Deliver to dock." (suiClose)="onChange($event)"></sui-inplace>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The closed text is role="button" and focusable. The input takes its accessible name from label, or from placeholder when there is no label.

<sui-inplace label="Memo" value="Fall supplies order. Deliver to dock."></sui-inplace>

Label and placeholder

placeholder shows in the faint tone while the value is empty, and it is what an empty field offers as its name. The label is the uppercase micro-label above the text.

<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-inplace label="Reference" value="PO-2211"></sui-inplace> <sui-inplace label="Reference" placeholder="Add a reference"></sui-inplace> <sui-inplace value="Untitled report" placeholder="Name this report"></sui-inplace> </div>

The host sets the type

The text and the field take the font of the host: size, weight, line height and family. A record title can be an inplace at the page title size, and both states share one box, so the page does not move when editing starts. With nothing set, the element is the body size, 13.5px.

Due
<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <h2 style="margin:0;font:var(--w-heavy) var(--text-2xl)/1.2 var(--font-sans);letter-spacing:var(--tracking-tight)"><sui-inplace value="Raise the gateway quota" placeholder="Untitled task"></sui-inplace></h2> <sui-inplace value="Untitled report" placeholder="Name this report"></sui-inplace> <span style="font:var(--w-regular) var(--text-sm)/1.45 var(--font-sans);color:var(--ink-muted)">Due <sui-inplace value="Sep 30" placeholder="Set a date"></sui-inplace></span> </div>

Opening from outside

editing is mutable and reflected, so a row menu can open the editor and a save can close every open one. A parent writing editing emits nothing, because it already knows.

<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-inplace id="ip-remote" label="Report title" value="August close checklist"></sui-inplace> <div style="display:flex;gap:8px"> <sui-button id="ip-rename" variant="outline" size="sm" icon="fa-regular fa-pen-to-square" label="Rename"></sui-button> <sui-button id="ip-done" variant="outline" size="sm" label="Close editor"></sui-button> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#ip-remote'); root.querySelector('#ip-rename').addEventListener('click', () => (el.editing = true)); root.querySelector('#ip-done').addEventListener('click', () => (el.editing = false)); }

In a form

The element is not form-associated. name goes on whichever native input is mounted, the text field while editing and a hidden one while closed, so the value posts once in both states.

Submit to see what posts.
<form id="ip-form" style="display:grid;gap:14px;max-width:340px"> <sui-inplace label="Memo" name="memo" value="Split freight across two invoices."></sui-inplace> <sui-inplace label="Reference" name="reference" placeholder="Add a reference"></sui-inplace> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" label="Save bill"></sui-button> </div> <output id="ip-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#ip-form'); const out = root.querySelector('#ip-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v || '(empty)'}`).join(' '); }); }

Events

suiOpen fires after the element enters edit mode itself and suiInput fires on every keystroke. suiClose fires before it leaves edit mode, with detail "pick" for a commit or "escape" for an abandoned edit, and suiSave follows an accepted commit with the stored value.

Idle.
<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-inplace id="ip-events" label="Account name" value="1010 Operating cash"></sui-inplace> <span id="ip-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#ip-events'); const log = root.querySelector('#ip-log'); el.addEventListener('suiOpen', () => { log.textContent = 'Editing.'; }); el.addEventListener('suiInput', (e) => { log.textContent = `Typing: ${e.detail || '(empty)'}`; }); el.addEventListener('suiClose', (e) => { log.textContent = `Leaving edit mode (${e.detail}).`; }); el.addEventListener('suiSave', (e) => { log.textContent = `Saved ${e.detail}.`; }); }

Disabled and read-only

disabled locks the text: no button role, no tab stop, no pencil, and a hidden control still posts its value, as a title the current user may not rename. readonly keeps the text focusable with its name and marks it aria-readonly, but opens nothing, for a record the user may see and not change.

<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-inplace label="Owner" value="Rhonda Mills" disabled></sui-inplace> <sui-inplace label="Account number" value="1010" readonly></sui-inplace> </div>

Holding a failed edit open

preventDefault on suiClose keeps the editor open and refocuses the input, which is how a failed validation stays on screen. Only the commit is vetoed here, so Escape still reverts and lets the user out.

Clear the text, then press Enter.
<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-inplace id="ip-veto" label="Account name" value="1010 Operating cash"></sui-inplace> <span id="ip-veto-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Clear the text, then press Enter.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#ip-veto'); const log = root.querySelector('#ip-veto-log'); el.addEventListener('suiClose', (e) => { if (e.detail !== 'pick' || el.value.trim()) { log.textContent = 'Accepted.'; return; } e.preventDefault(); log.textContent = 'An account needs a name. The editor stays open.'; }); }

Keyboard

KeyAction
Enter SpaceOn the closed text: opens the editor and focuses the input with the text selected.
EnterIn the input: commits the edit and returns focus to the text.
EscapeIn the input: reverts to the last saved value and returns focus to the text.
TabMoves focus on. The blur commits the edit, and focus stays where the user sent it.

API

<sui-inplace>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseLocks the text: no button role, no tab stop, no pencil, and a click, Enter or Space opens nothing. Reflected. A closed editor stays closed and a hidden control still posts the value, as disabled on a form control would not; a title the current user may not rename is the case.
editingeditingbooleanfalseWhether the editor is open. Mutable and reflected, so a parent can open the editor from its own control (a row menu's Rename) and close every open one when the record saves. A parent writing it emits nothing. An initial editing renders the input without stealing focus on mount.
labellabelstringstring | undefinedUppercase micro-label above the text. It is the input's accessible name.
namenamestringstring | undefinedForm control name, reflected. The element is not form-associated: the name goes on whichever native input is mounted, the text field while editing and a hidden one while closed, so the value posts once in both states.
placeholderplaceholderstringstring | undefinedShown in the faint tone while the value is empty. It also names the input when there is no label.
readOnlyreadonlybooleanfalseKeeps the text readable and focusable with its name, but opens nothing: the pencil goes and the text is aria-readonly. For a record the user may see but not change. Reflected, on the native readonly spelling.
valuevaluestring''Current text (the reference defaultValue). Mutable: updated on every keystroke and on save.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the element leaves edit mode itself; detail is 'pick' for a commit and 'escape' for an abandoned edit. Cancelable: preventDefault() keeps the editor open and refocuses it, which is how a parent holds a failed validation on screen. It does not bubble.
suiInputstringonSuiInput@sui-inputFires on every keystroke while editing; detail is the live value.
suiOpenvoidonSuiOpen@sui-openFires after the element enters edit mode itself (a click, Enter or Space, or edit()). It does not bubble, as no open-state event does.
suiSavestringonSuiSave@sui-saveFires when an edit commits (Enter or blur); detail is the saved value. Emitted after the matching suiClose('pick') is accepted.

Methods

MethodSignatureDescription
edit()edit() => Promise<void>Enters edit mode as a click would: focuses the input and emits suiOpen, so a row menu's Rename behaves like the person clicked the text. Writing editing opens the editor silently, for a parent that keeps the state itself.
setFocus()setFocus() => Promise<void>Moves keyboard focus to the text, or to the input while editing, without opening the editor.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also