Open demo

CSS code

HTML
<div><span class="lbl">File selector button</span><div class="card">
<div class="fi">
  <label class="fi-label" for="fi-invoice">Supplier invoice</label>
  <input class="fi-input" type="file" id="fi-invoice" name="invoice" accept=".pdf" aria-describedby="fi-invoice-hint">
  <p class="fi-hint" id="fi-invoice-hint">PDF only, up to 8 MB.</p>
</div></div></div>

<div><span class="lbl">Sizes: small, default, large</span><div class="card">
<div class="fi-sizes">
  <div class="fi"><label class="fi-label" for="fi-receipt">Receipt scan</label>
    <input class="fi-input sm" type="file" id="fi-receipt" name="receipt" accept="image/*,.pdf"></div>
  <div class="fi"><label class="fi-label" for="fi-contract">Signed contract</label>
    <input class="fi-input" type="file" id="fi-contract" name="contract" accept=".pdf"></div>
  <div class="fi"><label class="fi-label" for="fi-portfolio">Portfolio</label>
    <input class="fi-input lg" type="file" id="fi-portfolio" name="portfolio" accept=".pdf,.zip"></div>
</div></div></div>

<div><span class="lbl">Pill, multiple files</span><div class="card">
<div class="fi">
  <label class="fi-label" for="fi-photos">Site photos</label>
  <input class="fi-input pill" type="file" id="fi-photos" name="photos" accept="image/*" multiple aria-describedby="fi-photos-hint">
  <p class="fi-hint" id="fi-photos-hint">Pick as many as you need; the field shows how many you chose.</p>
</div></div></div>

<div><span class="lbl">Button and file name</span><div class="card">
<div class="fi" data-fi-name-field>
  <span class="fi-label" id="fi-cv-label">Curriculum vitae</span>
  <div class="fi-row">
    <input class="fi-sr" type="file" id="fi-cv" name="cv" accept=".pdf,.doc,.docx" aria-labelledby="fi-cv-label" aria-describedby="fi-cv-name fi-cv-hint">
    <label class="fi-btn" for="fi-cv"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m21.4 11.1-8.9 8.9a5.5 5.5 0 0 1-7.8-7.8l8.9-8.9a3.7 3.7 0 0 1 5.2 5.2l-8.9 8.9a1.8 1.8 0 0 1-2.6-2.6l8.2-8.2"/></svg>Choose file</label>
    <span class="fi-name" id="fi-cv-name" aria-live="polite">No file chosen</span>
  </div>
  <p class="fi-hint" id="fi-cv-hint">PDF or Word document.</p>
</div></div></div>
CSS
/* Styled file inputs. A-C restyle the native <input type="file"> through ::file-selector-button, so the browser
   still prints the chosen file name. D hides the input visually and draws a button (its <label>) plus a file name. */
.fi{--fi-accent:#3b5bdb;--fi-accent-ink:#2f4ac0;--fi-tint:#e8edfd;--fi-ink:#0f172a;--fi-muted:#5b6474;--fi-line:#8391a7;
  --fi-radius:10px;display:grid;gap:7px;color:var(--fi-ink)}
.fi-label{font-size:.92rem;font-weight:600}
.fi-hint{margin:0;font-size:.82rem;color:var(--fi-muted)}
.fi-input{display:block;width:100%;margin:0;padding:6px;font:inherit;font-size:.9rem;color:#334155;background:#fff;
  border:1px solid var(--fi-line);border-radius:var(--fi-radius);cursor:pointer;transition:border-color .15s}
.fi-input::file-selector-button{margin:0 14px 0 0;padding:8px 15px;font:inherit;font-weight:600;color:var(--fi-accent-ink);
  background:var(--fi-tint);border:0;border-radius:calc(var(--fi-radius) - 4px);cursor:pointer;transition:background-color .15s,color .15s}
.fi-input:hover{border-color:var(--fi-accent)}
.fi-input:hover::file-selector-button{background:var(--fi-accent);color:#fff}
.fi-input:focus-visible{outline:none;border-color:var(--fi-accent);box-shadow:0 0 0 3px #fff,0 0 0 5px var(--fi-accent)}
/* sizes */
.fi-sizes{display:grid;gap:16px}
.fi-input.sm{padding:4px;font-size:.8rem;--fi-radius:8px}
.fi-input.sm::file-selector-button{padding:5px 11px;margin-right:10px}
.fi-input.lg{padding:8px;font-size:1rem;--fi-radius:12px}
.fi-input.lg::file-selector-button{padding:11px 20px;margin-right:16px}
/* pill with a solid dark button */
.fi-input.pill{--fi-radius:999px;padding:5px 5px 5px 5px}
.fi-input.pill::file-selector-button{background:var(--fi-ink);color:#fff;padding:8px 18px}
.fi-input.pill:hover::file-selector-button{background:var(--fi-accent)}
/* D: button + file name. The input stays in the page for keyboard and screen readers, only visually hidden. */
.fi-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.fi-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.fi-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;font-size:.9rem;font-weight:600;color:#fff;
  background:var(--fi-accent);border-radius:var(--fi-radius);cursor:pointer;box-shadow:0 6px 14px -6px rgba(59,91,219,.6);transition:background-color .15s}
.fi-btn:hover{background:var(--fi-accent-ink)}
.fi-btn svg{width:18px;height:18px;flex:none}
.fi-sr:focus-visible + .fi-btn{outline:3px solid var(--fi-accent);outline-offset:3px}
.fi-name{display:inline-flex;align-items:center;gap:7px;min-width:0;font-size:.88rem;color:var(--fi-muted)}
.fi-name[data-filled]{color:var(--fi-ink);font-weight:600}
.fi-name[data-filled]::before{content:"";width:8px;height:8px;border-radius:50%;background:#15803d;flex:none}
@media (prefers-reduced-motion:reduce){.fi-input,.fi-input::file-selector-button,.fi-btn{transition:none}}
JavaScript
// Button + file name: prints the chosen file's name next to the button (the upload itself works without this).
document.querySelectorAll('[data-fi-name-field]').forEach(field => {
  const input = field.querySelector('input[type="file"]');
  const name = field.querySelector('#' + input.getAttribute('aria-describedby').split(' ')[0]);
  input.addEventListener('change', () => {
    const n = input.files.length;
    name.textContent = n === 0 ? 'No file chosen' : n === 1 ? input.files[0].name : n + ' files chosen';
    name.toggleAttribute('data-filled', n > 0);
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">File selector button</span><div class="card demo-card"><div class="card-body">
<div class="fi">
  <label class="form-label" for="fi-invoice">Supplier invoice</label>
  <input class="form-control" type="file" id="fi-invoice" name="invoice" accept=".pdf" aria-describedby="fi-invoice-hint">
  <div class="form-text" id="fi-invoice-hint">PDF only, up to 8 MB.</div>
</div></div></div></div>

<div><span class="lbl">Sizes: small, default, large</span><div class="card demo-card"><div class="card-body">
<div class="fi d-grid gap-3">
  <div><label class="form-label" for="fi-receipt">Receipt scan</label>
    <input class="form-control form-control-sm" type="file" id="fi-receipt" name="receipt" accept="image/*,.pdf"></div>
  <div><label class="form-label" for="fi-contract">Signed contract</label>
    <input class="form-control" type="file" id="fi-contract" name="contract" accept=".pdf"></div>
  <div><label class="form-label" for="fi-portfolio">Portfolio</label>
    <input class="form-control form-control-lg" type="file" id="fi-portfolio" name="portfolio" accept=".pdf,.zip"></div>
</div></div></div></div>

<div><span class="lbl">Pill, multiple files</span><div class="card demo-card"><div class="card-body">
<div class="fi">
  <label class="form-label" for="fi-photos">Site photos</label>
  <input class="form-control rounded-pill" type="file" id="fi-photos" name="photos" accept="image/*" multiple aria-describedby="fi-photos-hint">
  <div class="form-text" id="fi-photos-hint">Pick as many as you need; the field shows how many you chose.</div>
</div></div></div></div>

<div><span class="lbl">Button and file name</span><div class="card demo-card"><div class="card-body">
<div class="fi" data-fi-name-field>
  <span class="form-label d-block" id="fi-cv-label">Curriculum vitae</span>
  <div class="d-flex align-items-center flex-wrap gap-3">
    <input class="visually-hidden" type="file" id="fi-cv" name="cv" accept=".pdf,.doc,.docx" aria-labelledby="fi-cv-label" aria-describedby="fi-cv-name fi-cv-hint">
    <label class="btn btn-primary d-inline-flex align-items-center gap-2 mb-0" for="fi-cv"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m21.4 11.1-8.9 8.9a5.5 5.5 0 0 1-7.8-7.8l8.9-8.9a3.7 3.7 0 0 1 5.2 5.2l-8.9 8.9a1.8 1.8 0 0 1-2.6-2.6l8.2-8.2"/></svg>Choose file</label>
    <span class="fi-name d-inline-flex align-items-center gap-2 text-break" id="fi-cv-name" aria-live="polite">No file chosen</span>
  </div>
  <div class="form-text" id="fi-cv-hint">PDF or Word document.</div>
</div></div></div></div>
CSS
/* Bootstrap .form-control[type=file], restyled. Bootstrap already styles ::file-selector-button; these rules
   swap its grey button for a tinted one and add a focus ring. D uses .visually-hidden on the input and a .btn label. */
.fi{--fi-accent:#3b5bdb;--fi-accent-ink:#2f4ac0;--fi-tint:#e8edfd;--fi-line:#8391a7;--bs-border-radius:10px;
  --bs-border-radius-sm:8px;--bs-border-radius-lg:12px;--bs-secondary-color:#5b6474}
.fi .form-label{font-size:.92rem;font-weight:600;margin-bottom:7px;color:#0f172a}
.fi .form-text{margin-top:7px;font-size:.82rem;color:#5b6474}
.fi .form-control[type="file"]{padding:6px;font-size:.9rem;color:#334155;border-color:var(--fi-line);cursor:pointer;transition:border-color .15s}
.fi .form-control[type="file"]::file-selector-button{margin:0 14px 0 0;padding:8px 15px;font-weight:600;color:var(--fi-accent-ink);
  background:var(--fi-tint);border:0;border-radius:6px;cursor:pointer}
.fi .form-control[type="file"]:hover{border-color:var(--fi-accent)}
.fi .form-control[type="file"]:hover:not(:disabled):not([readonly])::file-selector-button{background:var(--fi-accent);color:#fff}
.fi .form-control[type="file"]:focus{border-color:var(--fi-accent);box-shadow:none}
.fi .form-control[type="file"]:focus-visible{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--fi-accent)}
.fi .form-control-sm[type="file"]{padding:4px;font-size:.8rem}
.fi .form-control-sm[type="file"]::file-selector-button{padding:5px 11px;margin-right:10px}
.fi .form-control-lg[type="file"]{padding:8px;font-size:1rem}
.fi .form-control-lg[type="file"]::file-selector-button{padding:11px 20px;margin-right:16px;border-radius:8px}
.fi .form-control.rounded-pill[type="file"]{padding:5px}
.fi .form-control.rounded-pill[type="file"]::file-selector-button{background:#0f172a;color:#fff;padding:8px 18px;border-radius:999px}
.fi .form-control.rounded-pill[type="file"]:hover:not(:disabled)::file-selector-button{background:var(--fi-accent)}
.fi .btn-primary{--bs-btn-bg:var(--fi-accent);--bs-btn-border-color:var(--fi-accent);--bs-btn-hover-bg:var(--fi-accent-ink);
  --bs-btn-hover-border-color:var(--fi-accent-ink);--bs-btn-padding-x:16px;--bs-btn-padding-y:10px;--bs-btn-font-size:.9rem;
  --bs-btn-font-weight:600;--bs-btn-border-radius:10px;box-shadow:0 6px 14px -6px rgba(59,91,219,.6)}
.fi .btn svg{width:18px;height:18px}
.fi .visually-hidden:focus-visible + .btn{outline:3px solid var(--fi-accent);outline-offset:3px}
.fi-name{font-size:.88rem;color:#5b6474}
.fi-name[data-filled]{color:#0f172a;font-weight:600}
.fi-name[data-filled]::before{content:"";width:8px;height:8px;border-radius:50%;background:#15803d;flex:none}
@media (prefers-reduced-motion:reduce){.fi .form-control,.fi .form-control::file-selector-button,.fi .btn{transition:none}}
JavaScript
// Button + file name: prints the chosen file's name next to the button (the upload itself works without this).
document.querySelectorAll('[data-fi-name-field]').forEach(field => {
  const input = field.querySelector('input[type="file"]');
  const name = field.querySelector('#' + input.getAttribute('aria-describedby').split(' ')[0]);
  input.addEventListener('change', () => {
    const n = input.files.length;
    name.textContent = n === 0 ? 'No file chosen' : n === 1 ? input.files[0].name : n + ' files chosen';
    name.toggleAttribute('data-filled', n > 0);
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-fi-muted uppercase">File selector button</span><div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,.05)] max-sm:p-[18px]">
<div class="grid gap-[7px]">
  <label class="text-[.92rem] font-semibold" for="fi-invoice">Supplier invoice</label>
  <input class="block w-full cursor-pointer rounded-[10px] border border-fi-line bg-white p-1.5 text-[.9rem] text-slate-700 transition-colors hover:border-fi-accent focus-visible:border-fi-accent focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-fi-accent)] focus-visible:outline-none motion-reduce:transition-none file:mr-3.5 file:cursor-pointer file:rounded-md file:border-0 file:bg-fi-tint file:px-[15px] file:py-2 file:font-semibold file:text-fi-accent-ink file:transition-colors hover:file:bg-fi-accent hover:file:text-white motion-reduce:file:transition-none" type="file" id="fi-invoice" name="invoice" accept=".pdf" aria-describedby="fi-invoice-hint">
  <p class="text-[.82rem] text-fi-muted" id="fi-invoice-hint">PDF only, up to 8 MB.</p>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-fi-muted uppercase">Sizes: small, default, large</span><div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,.05)] max-sm:p-[18px]">
<div class="grid gap-4">
  <div class="grid gap-[7px]"><label class="text-[.92rem] font-semibold" for="fi-receipt">Receipt scan</label>
    <input class="block w-full cursor-pointer rounded-lg border border-fi-line bg-white p-1 text-[.8rem] text-slate-700 transition-colors hover:border-fi-accent focus-visible:border-fi-accent focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-fi-accent)] focus-visible:outline-none motion-reduce:transition-none file:mr-2.5 file:cursor-pointer file:rounded file:border-0 file:bg-fi-tint file:px-[11px] file:py-[5px] file:font-semibold file:text-fi-accent-ink hover:file:bg-fi-accent hover:file:text-white" type="file" id="fi-receipt" name="receipt" accept="image/*,.pdf"></div>
  <div class="grid gap-[7px]"><label class="text-[.92rem] font-semibold" for="fi-contract">Signed contract</label>
    <input class="block w-full cursor-pointer rounded-[10px] border border-fi-line bg-white p-1.5 text-[.9rem] text-slate-700 transition-colors hover:border-fi-accent focus-visible:border-fi-accent focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-fi-accent)] focus-visible:outline-none motion-reduce:transition-none file:mr-3.5 file:cursor-pointer file:rounded-md file:border-0 file:bg-fi-tint file:px-[15px] file:py-2 file:font-semibold file:text-fi-accent-ink hover:file:bg-fi-accent hover:file:text-white" type="file" id="fi-contract" name="contract" accept=".pdf"></div>
  <div class="grid gap-[7px]"><label class="text-[.92rem] font-semibold" for="fi-portfolio">Portfolio</label>
    <input class="block w-full cursor-pointer rounded-xl border border-fi-line bg-white p-2 text-base text-slate-700 transition-colors hover:border-fi-accent focus-visible:border-fi-accent focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-fi-accent)] focus-visible:outline-none motion-reduce:transition-none file:mr-4 file:cursor-pointer file:rounded-lg file:border-0 file:bg-fi-tint file:px-5 file:py-[11px] file:font-semibold file:text-fi-accent-ink hover:file:bg-fi-accent hover:file:text-white" type="file" id="fi-portfolio" name="portfolio" accept=".pdf,.zip"></div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-fi-muted uppercase">Pill, multiple files</span><div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,.05)] max-sm:p-[18px]">
<div class="grid gap-[7px]">
  <label class="text-[.92rem] font-semibold" for="fi-photos">Site photos</label>
  <input class="block w-full cursor-pointer rounded-full border border-fi-line bg-white p-[5px] text-[.9rem] text-slate-700 transition-colors hover:border-fi-accent focus-visible:border-fi-accent focus-visible:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-fi-accent)] focus-visible:outline-none motion-reduce:transition-none file:mr-3.5 file:cursor-pointer file:rounded-full file:border-0 file:bg-slate-900 file:px-[18px] file:py-2 file:font-semibold file:text-white file:transition-colors hover:file:bg-fi-accent motion-reduce:file:transition-none" type="file" id="fi-photos" name="photos" accept="image/*" multiple aria-describedby="fi-photos-hint">
  <p class="text-[.82rem] text-fi-muted" id="fi-photos-hint">Pick as many as you need; the field shows how many you chose.</p>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-fi-muted uppercase">Button and file name</span><div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,.05)] max-sm:p-[18px]">
<div class="grid gap-[7px]" data-fi-name-field>
  <span class="text-[.92rem] font-semibold" id="fi-cv-label">Curriculum vitae</span>
  <div class="flex flex-wrap items-center gap-3">
    <input class="peer sr-only" type="file" id="fi-cv" name="cv" accept=".pdf,.doc,.docx" aria-labelledby="fi-cv-label" aria-describedby="fi-cv-name fi-cv-hint">
    <label class="inline-flex cursor-pointer items-center gap-2 rounded-[10px] bg-fi-accent px-4 py-2.5 text-[.9rem] font-semibold text-white shadow-[0_6px_14px_-6px_rgba(59,91,219,.6)] transition-[background-color] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-fi-accent hover:bg-fi-accent-ink motion-reduce:transition-none" for="fi-cv"><svg class="size-[18px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m21.4 11.1-8.9 8.9a5.5 5.5 0 0 1-7.8-7.8l8.9-8.9a3.7 3.7 0 0 1 5.2 5.2l-8.9 8.9a1.8 1.8 0 0 1-2.6-2.6l8.2-8.2"/></svg>Choose file</label>
    <span class="inline-flex min-w-0 items-center gap-[7px] text-[.88rem] text-fi-muted data-filled:font-semibold data-filled:text-slate-900 data-filled:before:size-2 data-filled:before:flex-none data-filled:before:rounded-full data-filled:before:bg-green-700 data-filled:before:content-['']" id="fi-cv-name" aria-live="polite">No file chosen</span>
  </div>
  <p class="text-[.82rem] text-fi-muted" id="fi-cv-hint">PDF or Word document.</p>
</div></div></div>
CSS
@theme {
  --color-fi-accent: #3b5bdb;
  --color-fi-accent-ink: #2f4ac0;
  --color-fi-tint: #e8edfd;
  --color-fi-line: #8391a7;
  --color-fi-muted: #5b6474;
}
JavaScript
// Button + file name: prints the chosen file's name next to the button (the upload itself works without this).
document.querySelectorAll('[data-fi-name-field]').forEach(field => {
  const input = field.querySelector('input[type="file"]');
  const name = field.querySelector('#' + input.getAttribute('aria-describedby').split(' ')[0]);
  input.addEventListener('change', () => {
    const n = input.files.length;
    name.textContent = n === 0 ? 'No file chosen' : n === 1 ? input.files[0].name : n + ' files chosen';
    name.toggleAttribute('data-filled', n > 0);
  });
});

About this set

Four ways to make the plain file input look like the rest of a form without losing what the browser does for free. File selector button restyles the native input through the ::file-selector-button pseudo-element: a bordered field with a tinted button that fills with the accent colour on hover, and the browser still prints the chosen file name beside it. Sizes shows the same field small, default and large, for dense tables, normal forms and touch screens. Pill, multiple files uses a rounded field with a dark button and the multiple attribute, so the field reports how many files were picked. Button and file name hides the input visually rather than removing it, draws a solid button from its label, and prints the chosen name next to it with four lines of JavaScript. Use the first three when the native field is enough and you only want it on brand; use the last when the design calls for a real button. In every variant the input stays in the page, so clicking the label, pressing Enter or Space, form submission and screen reader output work as they do on an unstyled input.

What’s included

  • Native input restyled through ::file-selector-button
  • Small, default and large sizes
  • Pill field for multiple files
  • Button and file name variant with a visually hidden input
  • Accept attributes and hint text on every field

Accessibility

Every input has a visible label, and hint text is joined with aria-describedby. The native fields show a two-colour focus ring on the field itself; the button and file name variant draws the ring on the button when the hidden input has keyboard focus, and the chosen name sits in a polite live region. Transitions switch off under prefers-reduced-motion.

Customise it

Change --fi-accent, --fi-accent-ink, --fi-tint and --fi-radius on .fi. In Bootstrap the same variables sit on .fi and .form-control-sm or .form-control-lg set the size; in Tailwind edit the fi colours in @theme and the file: utilities for the button.