WritingNo. 02UX, React
Build a logarithmic progress bar
Why a progress bar that lies a little at the start can be kinder than one that tells the exact truth, and how to build one.
2 min read
From the archive. Written a while ago, lightly dusted off.
When we build a progress bar to track a task, we usually build a linear one. A linear progress bar grows in proportion to the progress of the task: at 25% done, the bar shows 25%. Pretty straightforward, right? You might be thinking "Psst! That's how a progress bar works", and you'd be right. But should you always use it? Is there a better way?
Just want the code? Jump here.
What and why
Say you built an app that asks the user to do a series of tasks, maybe some onboarding steps or a survey. With each step, the progress bar... well, progresses! Progress bars show people how far they've come, and sometimes they're the reason people finish. But if there are a lot of steps and the bar barely moves over the first few, people lose motivation.Researchers Joseph Nunes and Xavier Drèze called this the endowed progress effect. A loyalty card that starts with two free stamps gets finished more often than a shorter card that starts empty. What if the first steps showed more progress? That's where a logarithmic progress bar comes in.
Move the slider from the start to the end and watch how the two bars behave. Then try a longer task.
The linear bar moves in a straight line with the slider. The logarithmic bar races ahead at the start and slows down near the end. That early push is often exactly what keeps people going.
The math
I know, I know. Math is boring. Bear with me. The math behind a linear progress bar is:
const progress = (currentStep / totalSteps) * 100;For a logarithmic progress bar we take the logarithm of the current step, using the total number of steps as the base:
JavaScript has Math.log, but it only gives the natural logarithm, the one with base . Luckily there's a formula to change the base:
So the code looks like this:
const getBaseLog = (base, arg) => Math.log(arg) / Math.log(base);
const progress = Math.round(getBaseLog(totalSteps, currentStep)) * 100;
const progress = Math.round(getBaseLog(totalSteps, currentStep) * 100); Building the component
With the math sorted, the component is small. I'm using React and Tailwind here.
export function ProgressBar({ current, total }) {
// log(0) is -Infinity, so treat the first step as empty.
const ratio = current === 0 ? 0 : Math.log(current) / Math.log(total);
const width = Math.round(Math.max(0, ratio) * 100);
return (
<div className="h-5 rounded border">
<div
style={{ width: `${width}%` }}
className="h-full bg-lime-500"
/>
</div>
);
}Is it lying?
When I first found out about logarithmic progress bars I was impressed and a little uneasy. Isn't this misleading? A bit, yes. But the bar still ends at 100% when the work is done, and it never goes backwards. People don't perceive progress as a straight line anyway, the same way two identical colors can look different depending on what's around them. I think of it as meeting people where their heads are, rather than where the numbers are.