Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Sorting "file10.txt" After "file2.txt": Natural Sort in One Line

C
Carlo Fontanos
· 2 min read

Every file manager, invoice list and version dropdown eventually shows this embarrassment:

invoice-1.pdf, invoice-10.pdf, invoice-11.pdf, invoice-2.pdf ...

Default string comparison walks character by character, and "1" < "2" settles the matter before the second digit gets a vote. Humans read the 10 as a number; the sort doesn't. The fix is built in:

const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });

files.sort((a, b) => collator.compare(a.name, b.name));
// invoice-1, invoice-2, invoice-10, invoice-11 ✓

numeric: true makes digit runs compare as numbers - the entire "natural sort" algorithm you'd otherwise regex together, delegated to the engine.

What the other option buys you

sensitivity: 'base' treats case and accents as equal: "apple", "Apple" and "Äpfel" sort together sensibly instead of uppercase clustering before lowercase (the ASCII-order artifact where "Zebra" beats "apple"). For user-facing lists this is nearly always what people expect. And because it's a Collator, locale rules come free - pass a locale tag and German umlauts or Swedish å/ä/ö sort per that language's conventions, with undefined meaning "the user's own locale".

Why Collator instead of localeCompare

// Works, but constructs collation machinery on EVERY comparison
names.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));

localeCompare with options is the same feature, but a sort makes O(n log n) comparisons and each call re-processes the options. Building one Collator and reusing its .compare is dramatically faster on big lists - same rule as every other Intl object: construct once, use many times. Bonus ergonomics: collator.compare is already a (a, b) function, so names.sort(collator.compare) just works.

Composing with real-world sorts

// Sort by folder first, then naturally by name - the file-manager special
entries.sort((a, b) =>
    (b.isFolder - a.isFolder) || collator.compare(a.name, b.name)
);

// Non-mutating, for state you don't own
const sorted = entries.toSorted((a, b) => collator.compare(a.name, b.name));

The || chain works because compare returns negative/zero/positive - zero falls through to the next criterion. (And toSorted keeps you from mutating shared arrays while you're at it.)

Support is universal and has been for years, including Node. PHP folks: the same fix exists server-side as natsort() and SORT_NATURAL - covered here - so your listings can sort correctly on whichever side renders them.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading