Micro-interactions
Inline Edit
Text that becomes its own input with nothing moving — one padded box, two contents, sized in CSS.
Install
bun add motion clsx tailwind-mergeThen copy 2 files into your project:
- components/registry/inline-edit/inline-edit.tsx
- lib/utils.ts
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| commitOn | "blur" | "explicit" | "blur" | Blur commits when focus leaves. Explicit grows a confirm and a cancel outside the box and treats losing focus as a cancel. Both are defensible, which is why it is a prop rather than a decision. |
| stiffness | number | 460 | Spring tension on the border, wash and ring arriving. This is the only thing that animates — everything else is already exactly where it will be. |
| damping | number | 32 | Spring friction on the same. Low overshoots the colour, which on a border reads as a flash rather than as bounce. |
| underline | "dotted" | "solid" | "none" | "dotted" | How the display state says it is editable. A text decoration rather than a border, because a border would have to be reserved in the editing state too — and none is the honest option for a row that already announces itself some other way. |
| padX | number | 8 | Horizontal padding. Shared by both states by construction: it lives on the shell, which never unmounts, so the two contents cannot disagree about it. |
| padY | number | 5 | Vertical padding. The one that shifts every row below it when an inline editor gets this wrong. |
| radius | number | 6 | Corner radius of the shell. |
| selectOnEdit | boolean | true | Select the whole value on entering edit, so typing replaces it. Off puts the caret where the click landed, which is the right behaviour for a long value being amended rather than renamed. |
| allowEmpty | boolean | false | Accept an empty commit. Off puts the previous value back, on the grounds that a blank field is a mis-click far more often than an intention. |
| showPencil | boolean | true | Pencil on hover and keyboard focus. Absolutely positioned outside the shell — anything rendered inside it changes the width of the box the component exists to hold still. |
| multiline | boolean | false | A textarea that grows by line instead of a single-line field. The growth is the same CSS trick either way: an invisible copy of the draft sized in the same grid cell, so nothing is measured in JavaScript. |
| accentColor | string (hex) | "#a855f7" | The border, wash and ring of the editing state, and the underline of the display state. Expected as a 6-digit hex: the transparent variants are built by suffix so the fade runs on one interpolation rather than between two colours. |
What It Demonstrates
- Morph
- One shape smoothly turns into another shape, e.g. Dynamic Island.
- Continuity transition
- A change that keeps the user oriented by visually connecting before and after. For example, making the same rectangle bigger and smaller.
- Spatial consistency
- Animating so an element keeps its identity and position across states, so users never lose track of where things went.
Related Components
Morph Dialog
A dialog that morphs open from its trigger via shared-layout animation, with elements that travel between card and panel.
Animated Hamburger
A three-bar hamburger that morphs to an X, driven by a toggle prop.
Hover List
A display-type list where a shared accent block springs between rows on hover while the text indents and inverts.
Rift
A draggable tear between two overlaid scenes — push past an edge and it resists, then snaps back.
Approach
A tile whose overlay enters from the exact edge your cursor crossed, and leaves the way you go.
Crossing
A gallery-to-detail crossing on the View Transitions API, where one element is the same object in both views.