A list of files with the right icon next to each name is read at a glance: the PDF is red, the spreadsheet green, the archive brown. A list where half the icons are a blank page is read line by line. The blank pages come from a handful of familiar cases: REPORT.PDF in capitals, backup.tar.gz shown as a plain gzip file, a photo from a phone called IMG_0042 with no extension at all, a CSV that the browser calls an Excel file, and a MIME type with ; charset=utf-8 on the end. This guide shows where a file's type really comes from, an order of rules that gives the right icon in all of those cases, and the server side, where the icon is the least of your worries. The JavaScript was run on Node 22 and the PHP on PHP 8.4, as printed.
#Where the type comes from
There are three sources, and none of them is the truth on its own.
- The name. The extension is chosen by whoever named the file. It is usually right, and it is the only thing you have for a file in a list that you have not opened.
- The browser's MIME type.
File.typein the browser is not read from the file. MDN's reference says it is assumed from the extension, that uncommon extensions give an empty string, and that the system's configuration, such as the Windows Registry, can give unexpected values even for common types. That is why a.csvfile on a Windows computer with Excel installed often arrives asapplication/vnd.ms-excel. When the form is sent, the same value goes to the server as the part'sContent-Type. - The bytes. Only the server can look inside the file, with a library such as PHP's
finfo, which recognises formats by their first bytes.
#An order of rules that works
For an icon, the name is the best source and the MIME type is the fallback. Check them in this order: a name ending in a slash is a folder; then double extensions such as .tar.gz; then the last extension in lower case, which also covers dotfiles such as .env; then the exact MIME type without its parameters; then the MIME family, so any image/... gets a generic image icon; and only then a generic file.
const EXT = { pdf: "pdf", docx: "docx", xlsx: "xlsx", csv: "csv", png: "png", jpg: "jpg", jpeg: "jpg",
heic: "heic", mp4: "mp4", zip: "zip", tgz: "tgz", json: "json", env: "env" };
const DOUBLE = { "tar.gz": "tgz", "tar.bz2": "bz2", "tar.xz": "xz" };
const MIME = { "application/pdf": "pdf", "image/heic": "heic", "text/csv": "csv",
"application/vnd.ms-excel": "xls", "application/zip": "zip" };
const FAMILY = ["image", "audio", "video", "text", "font", "model"];
export function iconName(fileName, mime = "") {
const name = String(fileName);
if (name.endsWith("/")) return "folder";
const base = name.split(/[\\/]/).pop().toLowerCase();
for (const [double, icon] of Object.entries(DOUBLE)) {
if (base.endsWith("." + double)) return icon;
}
const dot = base.lastIndexOf(".");
if (dot >= 0 && Object.hasOwn(EXT, base.slice(dot + 1))) return EXT[base.slice(dot + 1)];
const type = mime.split(";")[0].trim().toLowerCase();
if (Object.hasOwn(MIME, type)) return MIME[type];
const family = type.split("/")[0];
return FAMILY.includes(family) ? `kind-${family}` : "kind-file";
}
The tables here are cut short for the sample; a real set maps well over a hundred extensions. We ran it on ten names:
Annual report 2026.PDFwith no type givespdf: the extension in capitals still counts.backup.tar.gzgivestgz, not the gzip icon, because double extensions are checked first.IMG_0042withimage/heicgivesheic: no extension, so the exact MIME type decides.prices.csvwithapplication/vnd.ms-excelgivescsv: the name wins over the browser's guess.scan.weirdwithimage/x-portable-anymapgiveskind-image, and a barenoteswith no type giveskind-file.photos/is a folder,.envgivesenv, a Windows path ending inОтчет.docxgivesdocx, anddatawithtext/csv; charset=utf-8givescsv.
One small detail matters: the lookup uses Object.hasOwn(). With a plain ext in EXT, a file called x.constructor would find the object's own constructor function and return it as the icon name. With hasOwn it gets the generic icon.
#On the server, the icon is a label, not a check
After an upload you can do better than the name, because you have the bytes. Read the type from the content, show it next to the name, and keep the two decisions apart: the icon follows the name, because that is what the person sees and recognises; whether the file is accepted, stored or served follows the detected type and your other checks.
<?php
// The type a browser sends with an upload comes from the client; read the file itself instead.
function detected_type(string $path): string
{
return (new finfo(FILEINFO_MIME_TYPE))->file($path) ?: 'application/octet-stream';
}
function icon_row(string $originalName, string $storedPath, string $iconBase = '/icons/'): string
{
$type = detected_type($storedPath);
$icon = icon_name($originalName, $type); // the same rules as the JavaScript above
$name = htmlspecialchars($originalName, ENT_QUOTES);
return sprintf('<li><img src="%s%s.svg" width="24" height="24" alt=""> %s <small>%s</small></li>',
$iconBase, $icon, $name, htmlspecialchars($type, ENT_QUOTES));
}
icon_name() is a PHP port of the JavaScript function above. We ran icon_row() on a small PNG uploaded twice. Named holiday "photo", with no extension, it gets the image icon from the detected type image/png, and the quotes in the name come out escaped. Named <script>.pdf, it gets the PDF icon from its name, while the small print says image/png, and the name is shown as text, not run as a tag. A PDF icon on a PNG is harmless in a list; trusting the name to decide what the file is would not be. OWASP's File Upload Cheat Sheet goes further: allow only a list of extensions, do not trust the Content-Type header because it is easy to spoof, check the file's signature as well but not on its own, and store files under names your application generates.
#Showing the icon well
- When the file name is written next to the icon, the icon is decorative: give it an empty
alt="", as the W3C's images tutorial recommends, so screen readers do not read "PDF, Annual report 2026.PDF". - Set
widthandheighton the image, so the list does not jump as the icons load. - For a long list, one SVG sprite with
<use href="#pdf">is a single request instead of one per icon, and an outline style drawn incurrentColorfollows the text colour, also in dark mode. - If the icon shows the extension as text, that text needs contrast like any other: 4.5:1 for normal text under WCAG 2.2.
#A set of icons with the helpers
We made Faylche, a set of 202 file type icons: one for each of 172 extensions, one for each of 28 kinds of file and two folders, in four styles (a white page with a coloured label, flat colour, an outline in currentColor and a mini style drawn for 16 and 24 pixels), as SVG, ready PNG files and sprites. Its JavaScript and PHP helpers follow exactly the order of rules above, with 77 exact MIME types, and the tests check that the JavaScript, the PHP and the Python build give the same answer for the same names. White on every colour, and every colour on the page, has a contrast ratio of at least 4.5:1. The helpers were run with Node.js 22 and PHP 8.4 in the tests; design tools such as Figma were not opened. It is listed as Coming soon.
#Frequently asked questions
Why does the browser say my CSV file is an Excel file?
Because File.type comes from the system's settings for the extension, not from the file. On Windows with Excel installed, .csv is often registered as application/vnd.ms-excel. Decide the icon from the extension first.
Can I trust the MIME type of an uploaded file?
No. The type sent with an upload comes from the client. Read the type from the file's content on the server before you accept, store or serve it.
Which icon should a file with an unknown extension get?
Use the family of its MIME type when you have one, such as a generic image or audio icon, and a generic file icon when you have nothing.
Should a file icon have alt text?
Not when the file name is written next to it: then the icon is decorative and alt="" is right. An icon that stands alone needs a text alternative that says what the file is.
Comments
Comments
Be the first to leave a comment.
Leave a comment