Skip to content
PN Scripts

Development

Showing the right file icon from a file name or MIME type

  • PN Scripts
  • 8 min read

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.type in 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 .csv file on a Windows computer with Excel installed often arrives as application/vnd.ms-excel. When the form is sent, the same value goes to the server as the part's Content-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.PDF with no type gives pdf: the extension in capitals still counts.
  • backup.tar.gz gives tgz, not the gzip icon, because double extensions are checked first.
  • IMG_0042 with image/heic gives heic: no extension, so the exact MIME type decides.
  • prices.csv with application/vnd.ms-excel gives csv: the name wins over the browser's guess.
  • scan.weird with image/x-portable-anymap gives kind-image, and a bare notes with no type gives kind-file.
  • photos/ is a folder, .env gives env, a Windows path ending in Отчет.docx gives docx, and data with text/csv; charset=utf-8 gives csv.

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 width and height on 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 in currentColor follows 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.

#Sources

Keep reading

Keep reading

Comments

Comments

Be the first to leave a comment.

Leave a comment

Next step

pnscripts.com/contact

Talk to the team

Ask about an article, or tell us about a project you want built. We reply within one business day.

Write to us

The PN Scripts family

Other PN Scripts sites

Hosting, games and the blog each have their own site, run by the same company.

  • pnscripts.com

    PN Scripts

    Software engineering

    Custom web, mobile, API and game development, plus our open-source products and plugins.

  • games.pnscripts.com

    Games

    Games and game servers

    The home for PN Scripts games: free browser games you can play right now, with game servers coming soon.

  • hosting.pnscripts.com

    Hosting

    Hosting and infrastructure

    Shared hosting, KVM VPS, dedicated servers and domains, from the same company that builds your project.

  • blog.pnscripts.com

    Blog

    Articles and field notes

    Practical articles on software development, hosting, open source and games, written by the people who build them.

    You are here