PhoenixPaper.Input (PhoenixPaper v0.1.0)

Copy Markdown View Source

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

pp_input(assigns)

Attributes

  • id (:any) - Defaults to nil.
  • name (:any) - Defaults to nil.
  • label (:string) - Defaults to nil.
  • value (:any) - Defaults to nil.
  • 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 to false.
  • rows (:integer) - multiline only. Defaults to 3.
  • field (Phoenix.HTML.FormField) - Defaults to nil.
  • errors (:list) - Defaults to [].
  • helper_text (:string) - Defaults to nil.
  • disabled (:boolean) - Defaults to false.
  • paperize (:boolean) - Defaults to true.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted. Supports all globals plus: ["autocomplete", "autofocus", "form", "list", "max", "maxlength", "min", "minlength", "pattern", "placeholder", "readonly", "required", "step"].

Slots

  • start_adornment
  • end_adornment