Skip to content

Chat input

In review
Live · web

The whole flow (attach, type, record): everything floats on one layer

The chat input floats over the feed, and messages scroll under it.
And the thick cyan edge marks it as a floating layer?
Exactly. The §2 signature border rides the pill container. Type something: the circle morphs from mic to send.

Adaptive width, folded 360 · unfolded 840 · wide 1040 (scroll →): bubbles clamp to min(82%, bubbleMax), the pill clamps to bubbleMax and centers

Folded · 360
The column clamps, so content never stretches edge-to-edge.
And the pill lines up with the messages.
Unfolded · 840
The column clamps, so content never stretches edge-to-edge.
And the pill lines up with the messages.
Wide · 1040 (columnMax)
The column clamps, so content never stretches edge-to-edge.
And the pill lines up with the messages.

Sent 0 messages · pending 0 attachments · recording off. The + attaches (X removes), the mic pops the HUD above the pill (Cancel or the mic ends it), Enter sends, long-press the circle to flip send↔mic. The island is live.

The headline surface of the chat experience: a large, fully-rounded pill that FLOATS above the message cards, laid out [attach] [text field] [send/mic]. Canonical truth is the Android app’s ChatInputBar, brought up as the shipped Compose NockerlChatInput; the web NockerlChatInput mirrors it 1:1.

Three things make it read as the signature it is:

  • The §2 floating border. The pill container carries the signature thick cyan edge: border.width.floating (1.5px) in the full accent, the mark that trains the eye that a thick cyan border = a layer hovering above content (design laws §2). The border rides the container only; the controls inside keep their own treatment.
  • A field that is part of the pill. The text field is transparent inside the pill (ink on the chrome on-tokens, accent caret), deliberately not a recessed TextArea well (a well-in-a-pill would be a box-in-a-box). It grows to five lines, then scrolls.
  • The send↔mic morph (the one refinement). The trailing 48px accent circle is a single toggle: SEND when there’s text, MIC when empty, long-press to flip manually. The glyph cross-fades + scales (0.6 → 1) on the fast/standard motion tokens. One legible morph on interpolatable props (§4), never a same-color hard swap. Frozen under prefers-reduced-motion.
// Web: @dizyx/nockerl-react. Controlled value; the action circle auto-picks
// send (text present) or mic (empty); Enter sends, Shift+Enter breaks the line.
import { NockerlChatInput } from '@dizyx/nockerl-react';
export function Composer({ onSend, onMic, onAttach }: ComposerProps) {
const [draft, setDraft] = useState('');
return (
<NockerlChatInput
value={draft}
onValueChange={setDraft}
onSend={() => {
onSend(draft);
setDraft('');
}}
onMic={onMic}
onAttach={onAttach}
/>
);
}
PropTypeDefaultDescription
value *stringThe message text (controlled).
onValueChange *(next: string) => voidText edits.
onSend *() => voidFired when the action button is in SEND mode and activated (tap or Enter).
onMic *() => voidFired when the action button is in MIC mode and tapped.
placeholderstring'Message Nockerl…'The empty-field hint.
ariaLabelstring'Message'Accessible name for the text field (screen readers).
disabledbooleanfalseInert input + actions. Freeze ruling (task 2686/): react uses disabled (the DOM/react family idiom); compose keeps enabled (its idiom): semantics shared, casing platform-idiomatic per law §9.
onAttach(() => void) | undefinedOptional leading attach action; omit to hide the attach button.
attachIconReactNodeIconPlusThe attach glyph (default a plus; pass a paperclip from your set).
leadingAccessoryReactNodeGENERIC leading-accessory SLOT (task 2682) that replaces the built-in attach button entirely when provided (compose any control cluster; you own its a11y). The onAttach/attachIcon pair stays as the zero-config default.
attachmentsNockerlAttachment[]PENDING ATTACHMENTS MODEL (task 2682): when non-empty the composite renders the real NockerlAttachmentPopover floating directly ABOVE the pill (the canonical r5 integration), aligned to the pill's column. Empty (default) renders nothing: the resting DOM is byte-identical to the bare pill.
onRemoveAttachment((index: number) => void) | undefinedRemove handler for the attachments model (fired with the index).
contextAccessoryReactNodeGENERIC context/accessory SLOT (task 2682) that rides directly above the pill (below the attachments row): session chips, a context-gauge strip, the Recording HUD. Deliberately untyped (never session-typed props); empty renders nothing.
maxLinesnumber5Max visible text lines before the field scrolls (the native maxLines). Default 5.
classNamestringExtra class on the pill.

The moment something is attached, the pending thumbnails appear directly above the pill, the shipped NockerlAttachmentPopover (canonical source: the app’s PendingImageRow, brought up as the Compose NockerlAttachmentPopover). Try it live in the demo above: the + attaches, the badge removes, a send consumes them.

Each thumbnail is a small tile on the same L3 floating layer as the pill (messages scroll under both), clipped to the panel radius, traced by the warm agent-family edge at the shared border.width.floating weight, deliberately not cyan: the warm edge marks dismissable transient chrome, where the pill’s cyan §2 edge marks the fixed floating layer. A compact status-error badge overhangs the top-end corner to remove the attachment; the row scrolls horizontally on overflow.

The component owns only the frame (float + edge + badge). The caller supplies each image src and keeps the pick/decode/upload plumbing outside.

import { NockerlAttachmentPopover } from '@dizyx/nockerl-react';
<NockerlAttachmentPopover
attachments={pending} // [{ src, alt }]
onRemove={(i) => setPending((p) => p.filter((_, j) => j !== i))}
/>
PropTypeDefaultDescription
attachments *NockerlAttachment[]One entry per pending attachment, in display order.
onRemove *(index: number) => voidFired with the index whose remove badge was activated.
disabledbooleanfalseInert remove badges (freeze ruling per law §9: react uses disabled; compose keeps enabled).
classNamestringExtra class on the row.

The demo above is the whole experience on one stage: tap the mic and the Recording HUD pops directly above the pill, a sibling on the same L3 floating layer, one small gap, both carrying the §2 floating edge. The HUD leads with the brand mark (Nockerl is listening, never an anonymous red strip), then the pulsing status dot, the m:ss timer, the live 5-bar meter, and a ghost Cancel (the canonical order ratified with the native NockerlRecordingHud).

Stack order, bottom-up, everything floating over the scrolling feed:

  1. The pill: persistent, cyan §2 edge.
  2. The Recording HUD: transient, cyan edge, brand-led (present only while recording).
  3. The attachment popover: transient, warm dismissable edge (present only with pending attachments).

The voice state machine (recording → transcribing → streaming), the real microphone, and the upload plumbing all live in the app. The design system supplies the three floating surfaces and their shared grammar.

  • Enter sends on web (Shift+Enter breaks the line). The native pill has no return-to-send; unify brand expression, honor platform behavior.
  • Long-press flips the send↔mic override on both platforms (touch/pointer). Keyboard users always have auto mode: type → send, clear → mic.
  • The pill never renders a glow or colored shadow: depth is the L3 neutral lift + the top catch-light; the cyan edge is a border, not an emission (§1 + §2).