A Material Design text field (pp_input/1) with a floating label — pure
CSS, no JavaScript. Three variants: outlined (bordered box), filled
(filled background with an underline accent), and standard (underline
only, no box/background — and no shape, since there's nothing to round).
Accepts either a Phoenix Phoenix.HTML.FormField via field= (idiomatic
to_form/2 usage, same as the default core_components.ex input) or
plain name/value attrs.
<.pp_input label="Amount" name="amount">
<:start_adornment>$</:start_adornment>
</.pp_input>
<.pp_input label="Bio" name="bio" multiline rows={4} />multiline renders a <textarea rows={@rows}> instead of <input>,
reusing the exact same floating-label/peer-* mechanism — the label
doesn't care whether its peer is an <input> or a <textarea>, both are
ordinary form controls as far as :placeholder-shown/:focus are
concerned. There's no auto-growing-height JS here (rows is fixed) —
modern browsers support this via the plain CSS field-sizing: content
property with no JS at all, but it's new enough (2024+ Chromium/Firefox)
that it isn't relied on here; add it yourself via class if your
supported browsers cover it and you want it.
:start_adornment/:end_adornment (prefix/suffix content — an icon, a
unit like "kg", a button) sit as plain flex siblings outside the
input/label positioning box, not inside it — this means adding one never
requires touching the input's own padding or the label's left-*
position (both of which vary by variant/size already); the flex layout
just gives the adornment its own space and lets the input/label pair
occupy whatever's left.
The outlined notch
variant="outlined"'s border has a real gap cut into it around the
floated label — MUI's "notched outline" — not just a label floating on
top of an unbroken border line (an earlier version of this did that,
which reads as visibly wrong/off-brand next to any genuine Material
text field; caught from a side-by-side screenshot comparison, not by
reasoning about it). This is a real <fieldset>/<legend> — the actual
technique MUI itself uses, and a genuine browser rendering behavior, not
a CSS trick: browsers already draw a gap in a <fieldset>'s own border
around its <legend> natively, with zero custom CSS, if you just write
the HTML — PhoenixPaper.Shape's rounded-* plus a color are the only
things this component adds on top. The <legend> holds the same label
text as the real, visible floating <label> (in invisible text, so
it's never seen) — its only job is sizing the notch: max-width:0 at
rest so the border stays intact, transitioning to the label's natural
content width in the outlined-and-focused/outlined-and-filled state, the
same trigger conditions the real label's own top-2/text-xs shrink
already uses. Critically, the legend has zero horizontal padding at
rest (px-1 only applies alongside that same open-state
max-width:full, not unconditionally) — max-width can shrink an
element's content toward nothing, but never below its own padding
(a real CSS box-model rule, not a bug to work around some other way);
an earlier version had px-1 always on, which left a small permanent
gap in the resting, unfocused border exactly the width of that padding
— caught from a screenshot of the supposedly-closed state showing a
sliver of open border where there should have been none.
The <fieldset> is a sibling of the input (both descend from the same
wrapper <div>, absolutely positioned over it), not an ancestor of it —
<legend>'s native border-notching behavior is a product of the
fieldset's own default (non-flex) box layout, so the fieldset can't
double as the actual flex row arranging adornments/input/label without
losing that behavior entirely (verified empirically: setting
display: flex on the fieldset stops it from notching the legend at
all). Because of that sibling relationship, the usual peer-* trick
(which only reaches true siblings) can't connect the input to the
fieldset's nested <legend> two levels down — this uses has-* from
their common ancestor instead, scoped to the actual tag
(has-[input:not(:placeholder-shown)]/has-[textarea:not(:placeholder-shown)]),
not the unscoped has-[:not(:placeholder-shown)] — the unscoped form
would trivially match the <label> itself (which vacuously satisfies
"not placeholder-shown" the same way any non-form-control element does,
since it doesn't show a placeholder at all), keeping the notch
permanently open regardless of the input's real state. Caught before
shipping by working through what the selector actually matches, not
from a failing screenshot.
filled/standard don't get a notch — Material only notches the
bordered outlined variant; filled's underline and standard's
bare underline have no enclosing border to cut a gap into.
The legend's own margin-left has to line up with wherever the real
label actually starts, so its font size is kept in lockstep with the
label's own shrunk size (text-xs, not the resting text-sm) — an
earlier version left it at text-sm, sizing the notch for a bigger font
than what's actually showing and leaving dead space trailing the label
inside the gap, caught from a screenshot of a focused field. A
:start_adornment shifts the real label rightward (it's a flex sibling
ahead of the input/label column), but the <fieldset> is inset-0 on
the whole wrapper regardless — so has-[[data-pp-adornment=start]]
nudges the legend's margin out to roughly clear a short adornment (a
"$"-style symbol). This is a fixed approximation, not a measurement of
the adornment's actual rendered width (that would need JS, which this
component otherwise avoids entirely) — a wide adornment (a multi-word
label, a larger icon) can still throw the notch off; there's no
general pure-CSS fix for that without knowing the adornment's real
width at layout time.
Summary
Functions
Attributes
id(:any) - Defaults tonil.name(:any) - Defaults tonil.label(:string) - Defaults tonil.value(:any) - Defaults tonil.type(:string) - Defaults to"text".variant(:string) - Defaults to"outlined". Must be one of"outlined","filled", or"standard".size(:string) - Defaults to"medium". Must be one of"medium", or"small".color(:string) - Defaults to"primary". Must be one of"primary","secondary","tertiary", or"error".shape(:atom) - corner radius token, see PhoenixPaper.Shape — ignored for variant="standard". Defaults to:sm. Must be one of:none,:xs,:sm,:md,:lg,:xl, or:full.multiline(:boolean) - renders a <textarea rows={@rows}> instead of <input>. Defaults tofalse.rows(:integer) - multiline only. Defaults to3.field(Phoenix.HTML.FormField) - Defaults tonil.errors(:list) - Defaults to[].helper_text(:string) - Defaults tonil.disabled(:boolean) - Defaults tofalse.paperize(:boolean) - Defaults totrue.class(:any) - Defaults tonil.- Global attributes are accepted. Supports all globals plus:
["autocomplete", "autofocus", "form", "list", "max", "maxlength", "min", "minlength", "pattern", "placeholder", "readonly", "required", "step"].
Slots
start_adornmentend_adornment