Files
Htmx/docs/Components/FileInput.md
T
2026-05-05 23:55:26 +05:00

6.3 KiB

FileInput

A styled file upload field. Use it when you need users to attach files to a form — profile pictures, documents, CSV imports, and so on.


Quick example

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

All the options

public FileInput(
    string id,
    string name         = "",
    string accept       = "",
    bool   multiple     = false,
    string label        = "",
    string description  = "",
    string extraClasses = "",
    string hxAttrs      = "")
Parameter What it does
id The element id. Also used by the <label for="...">.
name Form field name — required if you want the file submitted with the form.
accept MIME types or extensions to filter the picker, e.g. "image/*" or ".pdf,.docx". Does not validate server-side.
multiple Allow selecting more than one file at a time.
label Visible text label above the field.
description Hint text below the field (e.g. "Max 5 MB").
extraClasses Additional Tailwind classes on the <input> element.
hxAttrs Extra HTML attributes appended verbatim (HTMX, data-*, etc.).

Real-world examples

Multiple document attachments

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

Auto-upload on file selection (HTMX)

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"""")

When using HTMX for file uploads, always include hx-encoding="multipart/form-data" — HTMX does not infer it from the input type.

Reading uploaded files in a handler

public static IResult Handle(HttpContext ctx, IFormFile? avatar)
{
    if (avatar is null || avatar.Length == 0)
        return Results.BadRequest("No file uploaded");

    // validate file type server-side (accept= only filters in the browser)
    var allowed = new[] { "image/jpeg", "image/png", "image/gif" };
    if (!allowed.Contains(avatar.ContentType))
        return Results.BadRequest("Invalid file type");

    using var stream = avatar.OpenReadStream();
    // save the file...
    return Results.Ok();
}

How it works

FileInput renders a standard <input type="file">. The browser's built-in "Choose file" button is styled using ::file-selector-button CSS pseudo-element (via Tailwind's file: prefix) so it matches the rest of the UI.


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");
    }
}