File Input

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleApache-2.0
Installation mix mishka.ui.gen.headless file_input
Source View Source

Overview of the Phoenix and LiveView Headless File Input Component

The Mishka Chelekom Headless File Input is the browser's own file picker, wired for plain forms and for LiveView uploads. It ships no styling. Unlike the other form controls it is not wrapped: the input is the root, so your classes go straight on it. That matters because the browser's button, ::file-selector-button, only exists on the input itself, and Tailwind's file: variant is how you reach it.

There are three ways to wire it up. Pass upload={@uploads.avatar} and the component renders live_file_input, so LiveView owns the name, the accepted types and the multiple flag. Pass field={@form[:attachment]} inside a Phoenix form to take the id and name from the form. Or pass name for a plain input. The input reflects its state in data-invalid and data-disabled, and class lands on the input itself.

Base UI File Input example - native picker with a styled button (Phoenix headless)

Base UI File Input example - native picker with a styled button (Phoenix headless)

heex
<.file_input
  id="baseui-file-hero"
  name="attachment"
  class="w-full max-w-80 rounded-md border border-neutral-200 text-sm text-neutral-950 dark:border-neutral-800 dark:text-white file:mr-3 file:cursor-pointer file:border-0 file:bg-neutral-950 file:px-3 file:py-1.5 file:text-sm file:text-white dark:file:bg-white dark:file:text-neutral-950"
/>

Base UI File Input example - inside a field with a description (Phoenix headless)

Base UI File Input example - inside a field with a description (Phoenix headless)

PNG or JPG, up to 2MB.

heex
<.field
  :let={f}
  id="baseui-file-field"
  name="avatar"
  label="Avatar"
  class="flex w-full max-w-80 flex-col gap-1"
  label_class="text-sm font-medium text-neutral-950 dark:text-white"
  description_class="text-xs text-neutral-500"
>
  <.file_input
    id={f.id}
    name={f.name}
    accept="image/*"
    describedby={f.describedby}
    class="w-full max-w-80 rounded-md border border-neutral-200 text-sm text-neutral-950 dark:border-neutral-800 dark:text-white file:mr-3 file:cursor-pointer file:border-0 file:bg-neutral-950 file:px-3 file:py-1.5 file:text-sm file:text-white dark:file:bg-white dark:file:text-neutral-950"
  />
  <:description>PNG or JPG, up to 2MB.</:description>
</.field>

Uploads, forms and errors

With field, the id and name come from the form field. With multiple the name gets a [] suffix, so Plug builds a list. accept is a comma separated list of file types, and upload ignores it because LiveView owns the list. Pass name only for a plain input, since upload and field supply theirs.

heex
<.file_input upload={@uploads.avatar} class="..." />

<.file_input field={@form[:attachment]} class="..." />

<.file_input field={@form[:gallery]} multiple accept="image/*" class="..." />

<.file_input name="attachment" class="..." />

errors is a list of messages. A non-empty list marks the input invalid, with aria-invalid="true" and data-invalid. The input does not print the messages, so show them in a Field. With field, errors count only after the user has touched the input. With upload, errors on the upload itself, such as too many files, always mark it invalid. describedby sets aria-describedby. disabled sets data-disabled and, on a plain input, the native disabled attribute. required makes a plain input required.

Parts & data attributes

There is one part: the input. It carries data-part="input" and the chelekom-file-input class.

Partdata-partState / ARIAPurpose
Input (root)inputaria-invalid, aria-describedby, data-invalid, data-disabledThe native file input. It is the root, so classes and the file: variants apply to it directly.

Accessibility

The control is the native file input, so Tab focuses it and Enter or Space opens the picker. It needs a visible label, which a Field provides, and describedby should point at hints and error text. An invalid input is announced through aria-invalid. A plain input with disabled is skipped by the tab order.