Files
Htmx/docs/Components/FileInput.md
T
2026-05-04 19:57:48 +05:00

7.0 KiB

FileInput

A styled file upload field with an optional visible label and description. Supports accept MIME types, multiple file selection, and HTMX attributes for server-driven interactions.


HTML structure

div.flex.flex-col.gap-1.5
  label[for={id}].text-sm.font-medium        ← omitted when label is empty
    {label text}
  input[type=file, id, name, accept, class, $$Multiple$$, $$HxAttrs$$]
  p.text-sm.text-muted-foreground            ← omitted when description is empty
    {description text}

CSS mechanics

Class Effect
file:mr-4 file:py-2 file:px-4 file:rounded-md Styles the browser's "Choose file" button via ::file-selector-button
file:border-0 file:bg-primary file:text-primary-foreground Gives the file button the primary color
file:text-sm file:font-semibold file:cursor-pointer Consistent text treatment
hover:file:bg-primary/90 Slight darkening on hover
w-full rounded-md border border-input bg-background text-sm Full-width field with border

Constructor signature

public FileInput(
    string id,
    string name         = "",
    string accept       = "",
    bool   multiple     = false,
    string label        = "",
    string description  = "",
    string extraClasses = "",
    string hxAttrs      = "")
Parameter Description
id Element id and label for target
name Form field name
accept MIME types or file extensions, e.g. "image/*" or ".pdf,.docx"
multiple Allows selecting more than one file
label Visible label above the field
description Helper text below the field
extraClasses Additional Tailwind classes on the input
hxAttrs Verbatim HTMX / data attributes appended to the input

Usage examples

Basic single file

new FileInput(
    id:   "avatar",
    name: "avatar",
    accept: "image/*",
    label: "Profile picture",
    description: "PNG, JPG or GIF up to 2 MB")

Multiple files

new FileInput(
    id:       "attachments",
    name:     "attachments",
    accept:   ".pdf,.docx,.xlsx",
    multiple: true,
    label:    "Attachments",
    description: "Select one or more documents")

HTMX auto-upload on change

new FileInput(
    id:       "import-csv",
    name:     "csv",
    accept:   ".csv",
    label:    "Import CSV",
    hxAttrs:  """hx-post="/import" hx-encoding="multipart/form-data" hx-target="#result" hx-trigger="change"""")

No label

new FileInput(id: "doc", name: "document", accept: ".pdf")

Tips and tricks

  • accept filters in the browser's file picker dialog but does not validate server-side — always validate the uploaded file type in your handler.
  • For HTMX file uploads set hx-encoding="multipart/form-data" in hxAttrs; HTMX does not infer encoding from the input type.
  • Multiple files are bound as a list: IFormFileCollection or List<IFormFile> in the handler. FromForm attribute on the command record field is required.
  • To show a preview of the selected image before upload, add a small JS snippet that listens to the change event and sets src on an <img> element via URL.createObjectURL(e.target.files[0]).
  • extraClasses is added to the <input> element, not the wrapper <div> — use it for overriding width, borders, or custom ring colors.
  • extraClasses is added to the <input> element, not the wrapper <div> — use it for overriding width, borders, or custom ring colors.

Complete page example

Templates/UploadPage.htmx

<div class="max-w-md mx-auto py-10">
  <h1 class="text-2xl font-bold mb-6">Upload document</h1>
  <form method="post" action="/upload" enctype="multipart/form-data">
    $$AntiforgeryToken$$
    <div class="space-y-4 mb-6">
      $$FileField$$
    </div>
    $$SubmitBtn$$
  </form>
  $$Result$$
</div>

Templates/UploadPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class UploadPage : UploadPageBase
{
    private readonly IHtmxComponent _fileField;
    private readonly IHtmxComponent _submitBtn;
    private readonly IHtmxComponent _result;
    private readonly byte[] _afToken;

    public UploadPage(IAntiforgery af, HttpContext ctx, string? uploadedFileName = null, string? error = null)
    {
        var tokens = af.GetAndStoreTokens(ctx);
        _afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();

        _fileField = new Components.FileInput(
            id:          "document",
            name:        "document",
            accept:      ".pdf,.docx",
            label:       "Select a file",
            description: "PDF or Word document, max 10 MB");
        _submitBtn = new Components.Button("Upload", type: "submit");
        _result    = error is not null
            ? new Components.Alert(title: "Upload failed", description: error, variant: "destructive")
            : uploadedFileName is not null
                ? new Components.Alert(title: "Uploaded!", description: $"Saved as: {uploadedFileName}")
                : HtmxEmpty.Instance;
    }

    protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
    protected override void RenderFileField(HtmxRenderContext ctx)        => _fileField.Render(ctx.Next());
    protected override void RenderSubmitBtn(HtmxRenderContext ctx)        => _submitBtn.Render(ctx.Next());
    protected override void RenderResult(HtmxRenderContext ctx)           => _result.Render(ctx.Next());
}

GET + POST handlers

[Handler]
[MapGet("/upload")]
public static partial class GetUploadHandler
{
    public record Query();
    private static Task<IResult> HandleAsync(Query _, HttpContext ctx, IAntiforgery af, CancellationToken ct)
        => ctx.WriteHtmxPage(new UploadPage(af, ctx), title: "Upload");
}

[Handler]
[MapPost("/upload")]
[DisableRequestSizeLimit]
public static partial class PostUploadHandler
{
    public record Command([property: FromForm] IFormFile? Document = null);

    private static async Task<IResult> HandleAsync(
        [AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
    {
        if (cmd.Document is null)
        {
            return await ctx.WriteHtmxPage(
                new UploadPage(af, ctx, error: "No file was selected."), title: "Upload");
        }

        var allowedTypes = new[] { ".pdf", ".docx" };
        var ext = Path.GetExtension(cmd.Document.FileName).ToLowerInvariant();
        if (!allowedTypes.Contains(ext))
        {
            return await ctx.WriteHtmxPage(
                new UploadPage(af, ctx, error: "Only PDF and Word files are allowed."), title: "Upload");
        }

        var safeName = Path.GetRandomFileName() + ext;
        var savePath = Path.Combine("uploads", safeName);
        await using var stream = File.Create(savePath);
        await cmd.Document.CopyToAsync(stream, ct);

        return await ctx.WriteHtmxPage(
            new UploadPage(af, ctx, uploadedFileName: safeName), title: "Upload");
    }
}