Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bsfp-card">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-bold mb-0">Complete your profile</h6>
<span class="small fw-semibold text-muted" id="bsfpPercent">0%</span>
</div>
<div class="progress mb-3" style="height:6px;">
<div class="progress-bar" id="bsfpBar" role="progressbar" style="width:0%"></div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold mb-1">Full name</label>
<input type="text" class="form-control form-control-sm bsfp-field" id="bsfpName">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold mb-1">Job title</label>
<input type="text" class="form-control form-control-sm bsfp-field" id="bsfpTitle">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold mb-1">Company</label>
<input type="text" class="form-control form-control-sm bsfp-field" id="bsfpCompany">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold mb-1">Short bio</label>
<textarea class="form-control form-control-sm bsfp-field" id="bsfpBio" rows="2"></textarea>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold mb-1">Profile photo</label>
<input type="file" class="form-control form-control-sm bsfp-field" id="bsfpPhoto">
</div>
<button type="button" class="btn btn-dark btn-sm fw-bold w-100" id="bsfpSubmit" disabled>Save profile</button>
</div>
</div>
</div>.bsfp-card { width: 360px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
#bsfpBar { transition: width .2s ease; }const fields = Array.from(document.querySelectorAll('.bsfp-field'));
const bar = document.getElementById('bsfpBar');
const percentEl = document.getElementById('bsfpPercent');
const submitBtn = document.getElementById('bsfpSubmit');
function isFilled(field) {
if (field.type === 'file') return field.files && field.files.length > 0;
return field.value.trim() !== '';
}
function update() {
const filled = fields.filter(isFilled).length;
const pct = Math.round((filled / fields.length) * 100);
bar.style.width = pct + '%';
bar.className = 'progress-bar' + (pct === 100 ? ' bg-success' : '');
percentEl.textContent = pct + '%';
submitBtn.disabled = pct < 100;
}
fields.forEach(field => {
field.addEventListener(field.tagName === 'INPUT' && field.type === 'file' ? 'change' : 'input', update);
});
update();Bootstrap Form Progress Indicator — Free HTML CSS JS Snippet
Bootstrap Form Progress Indicator · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Bootstrap Form Progress Indicator — HTML, CSS & JavaScript

This is a genuinely different pattern from a multi-step wizard like bootstrap-stepper-wizard-form — every field here lives on one single page at once, and the progress bar exists purely to communicate how much of that one page is filled in, not which of several discrete steps the user is currently on.
isFilled() is the one place that has to correctly handle two genuinely different field types: a text/textarea field counts as filled when its trimmed value is non-empty, while a file input has no meaningful .value to check at all — it's filled when .files.length > 0. Getting this branch wrong (checking .value on a file input, which browsers deliberately keep unreliable for security reasons) is a common bug in a homemade version of this pattern.
Every field type also needs the right event to actually notice a change — text inputs and textareas fire input on every keystroke, but a file input only ever fires change, never input, when a file is selected. The listener setup branches on that explicitly rather than attaching the same event name to every field and hoping it works, which is exactly the kind of subtle cross-field-type bug that's easy to miss until a coworker actually tries filling in the file field and the bar doesn't move.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Hand this snippet to an AI coding assistant like Claude and ask it to weight fields differently (e.g. required fields worth more than optional ones) instead of treating all fields equally, or to add a small checklist breakdown next to the bar showing exactly which fields are still missing.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Bootstrap 5.3 single-page form with a live completion-percentage progress bar, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.
Requirements:
- A form with at least 5 fields of mixed types, including at least one text input, one textarea, and one file input.
- A progress bar and percentage label above the fields, recomputed live as the user fills in each field, based on how many of the total fields currently qualify as "filled".
- Correctly detect a filled file input using its files.length property (not its value), and listen for its "change" event rather than "input" since file inputs never fire "input" on selection.
- The progress bar should turn a distinct success color once it reaches 100%, and a submit button should stay disabled until then, both derived from the same completion count.
- Clearing any previously filled field must immediately reduce the percentage and re-disable the submit button if it drops below 100%.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Load the snippetThe bar sits at 0%, "Save profile" is disabled, and every field is empty.
- 2Type your nameThe bar jumps to 20% immediately as you type the first character.
- 3Fill in job title, company, and bioThe percentage climbs with each field, updating live on every keystroke.
- 4Select a profile photo fileThe bar reaches 100%, turns green, and "Save profile" becomes enabled.
- 5Clear one of the text fieldsThe percentage immediately drops back below 100%, and Save disables again.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A file input's .value is deliberately kept unreliable by browsers for security reasons and cannot be used to detect a selection — .files.length is the correct, reliable way to check whether a file has actually been chosen.
A file input never fires the "input" event on selection (unlike text fields, which fire it on every keystroke) — it only fires "change", so a listener attached with the wrong event name would silently never detect a file selection at all.
This demo weighs all five fields equally for simplicity; a real implementation might weight fields differently (e.g. required fields counting more than optional ones) depending on what "100% complete" should actually mean for that specific form.
Yes. Track each field's value (and the file input's selected-file state) in component state, and derive the percentage and submit-disabled flag from that state on every render — the isFilled per-field-type logic carries over directly.