Input
A field shaped like an Item: an optional icon, the text itself, and a box that reacts to what the input inside it is doing.
API
Props
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "div" | the element or component to render |
active? | boolean | false | keeps the box highlighted regardless of focus; also exposed as data-margo-active |
clickable? | boolean | true | when false, the box and everything in it ignore the pointer and the tab order |
…rest | props of as | — | typed against the chosen element |
Input.Icon
| prop | type | default | |
|---|---|---|---|
icon? | ReactNode | <MdSearch /> | the glyph; the default is the lens |
className? | string | — | merged over the defaults |
Input.Text
| prop | type | default | |
|---|---|---|---|
className? | string | — | merged over the defaults |
…rest | props of as | — | typed against the chosen element |
Examples
Usage
An Input is a full width box: at rest it is quiet, it takes a border on hover, and it turns primary while the field inside it holds focus. There is no ripple and no press animation — a field is typed into, not pressed.
<Input>
<Input.Text placeholder="search the docs" />
<Input.Icon />
</Input>A leading icon
The icon carries the auto margin, so where you mount it is where it lands: mount it before the text and it leads the field instead of trailing it. Nothing else changes — the text still takes the free space, because it is the flexible part of the row.
<Input>
<Input.Text placeholder="trailing" />
<Input.Icon />
</Input>
<Input>
<Input.Icon className="-translate-x-0.5" />
<Input.Text placeholder="leading" />
</Input>States
active keeps the box highlighted whether or not anything is focused — for a field that stays open, a filter with a value in it, a search bar that owns the screen. clickable set to false takes the whole box out of reach — no pointer, no caret, no tab stop — while leaving it looking untouched, the shape of a field waiting on something. disabled goes on Input.Text, not on the box: the box notices and dims itself.
<Input active>
<Input.Text placeholder="active" />
<Input.Icon />
</Input>
<Input clickable={false}>
<Input.Text placeholder="inert" />
<Input.Icon />
</Input>
<Input>
<Input.Text placeholder="disabled" disabled />
<Input.Icon />
</Input>Details
The state lives in the field
Item takes its states as props because the row itself is the control. Here it is not: the control is the native input, and it already knows whether it is focused or disabled. The box reads that state off its own subtree with focus-within and has instead of being told about it, which is why there is no focused prop to keep in sync and nothing to wire on blur.
export const inputBaseClassName = `…
focus-within:border-primary focus-within:text-on-main
has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-40`;One consequence worth knowing: the highlight follows real focus, so it survives autofill, a programmatic focus() and a click that lands on the padding — none of which a React state would have caught for free.
Taking it out of reach
The thing to take out of reach here is nested: dropping pointer events on the box would still leave the field one Tab away, and a caret blinking inside something meant to be unavailable is worse than no treatment at all. A tabIndex of -1 on the box would not help either — it is not the box that takes focus.
<Tag inert={!clickable || undefined} …>So clickable sets inert on the box, which the browser applies to the entire subtree: no clicks, no focus, no text selection, and the content is hidden from assistive technology — one attribute instead of a prop threaded down to every part. Button and Item do the same, which is why none of the three carries a class for it.
One detail worth spelling out: inert is a boolean attribute, so it either exists or it does not — there is no false value of it. The prop is a boolean, the attribute is a presence, and the two are bridged by falling back to undefined rather than passing false along.
inert={!clickable}
inert={!clickable || undefined}No ripple, no press
Both are deliberate omissions. A ripple answers a click that did something; clicking a field only moves the caret, and an ink spread under the text is noise while you are reading what you typed. The same goes for the scale on press: a box that shrinks under the pointer and then holds a caret reads as a button that failed to do anything.
Labelling it
A placeholder is not a label — it disappears exactly when the user needs it. Put a Label above the field and point it at the input with htmlFor, so clicking the caption focuses the field. For a search bar, where the lens is the label, render the box itself as a label instead.
<Label htmlFor="email">email</Label>
<Input>
<Input.Text id="email" placeholder="you@domain.com" />
<Input.Icon icon={<MdMail />} />
</Input>The box dims itself when the input inside it is disabled, but the caption stands outside that subtree and cannot see it, so disabled on the Label forces the same dimming.
<Label htmlFor="city" disabled>city</Label>
<Input>
<Input.Text id="city" placeholder="unavailable" disabled />
<Input.Icon icon={<MdMail />} />
</Input>