Skip to content
DocsSupportPricing
Roadmap (has updates coming soon)XFigma3.7K
Sign inGet All-Access
Overview
  • Introduction
  • Get Started
  • License Setup
  • Styling
  • Registry
  • MCP Server
  • Embed
  • Agent Skills
  • llms.txt
  • RTL
  • Changelogv2.8.0
  • YouTube
MCP Server
  • Claude
  • CodexCodexCodex
  • Cursor
  • Grok
  • Conductor
  • v0
  • Lovable
  • Replit
  • Bolt
  • OpenCode
  • VS Code
  • GitHub Copilot
  • Kilo Code
  • Zed
  • Antigravity
  • WSWindsurf
  • CLCline
  • Gemini CLI
  • AMAmp
  • JBJetBrains Junie
Components
  • Alert
  • Autocomplete
  • Badge
  • Cascader
  • Code Block
  • Color PickerNew Color Picker docs
  • Data GridData Grid adds grouped and spanning headers, scrolls faster, and its scrollbar stops above a sticky footer
  • Date Selector
  • Emoji PickerNew Emoji Picker docs
  • Event Calendar
  • File TypeNew File Type docs
  • File Upload
  • Filters
  • Frame
  • Gantt
  • Icon Stack
  • Icon Tile
  • Kanban
  • Number Field
  • Phone Input
  • Rating
  • Scrollspy
  • Signature Pad
  • Sortable
  • StepperAdded 29 new Stepper examples, 44 in all
  • Time Picker
  • Timeline
  • Tree

Application

  • App ShellAdded 4 new App Shell blocks
  • Auth
  • Card
  • Dashboard
  • Dialog
  • Empty State
  • Event Calendar
  • Flow
  • Form
  • Gantt
  • Kanban Board
  • List
  • Navbar
  • Onboarding
  • Profile
  • Rich Text Editor
  • Schedule
  • Settings
  • Sheet
  • Stats
  • Timeline
  • Whiteboard
  • Wizard

Data Grid

  • Base
  • AdvancedNew Advanced category with 3 Data Grid blocks
  • Columns
  • Drag & Drop
  • Editing
  • Expansion
  • Filtering
  • Grouping
  • Virtualization

Charts

  • Area Chart
  • Bar Chart
  • Composed Chart
  • Line Chart
  • Pie Chart
  • Radial Chart
  • Sparkline
  • Status Tracker

AI & Agents

  • AI Chat
  • Agent Activity

Solutions

  • Dev Ops
  • AI Ops
  • CRM
  • Agents
  • Analytics
  • Billing
  • Bookings
  • Files
  • Inventory
  • Users

Templates

  • E-commerce
  • SaaS
  • Dashboard
  • Landing
  • All templates

eCommerce

  • Category Card
  • Checkout
  • Comparison
  • Coupon
  • Filter Sidebar
  • Product Card
  • Product Detail
  • Product Grid
  • Receipt
  • Review
  • Shopping Cart
  • Wishlist
  • Shop Hero

Marketing

  • Blog
  • Compare
  • Contact
  • CTA
  • FAQ
  • Hero
  • How It Works
  • Pricing

Resources

  • Components
  • Blocks
  • Icons
  • MCP for Agents
  • Docs
  • Support
  • Pricing
  • Tailwind Plus Alternative
  • Roadmap(has updates coming soon)
  • AffiliateSoon

Legal

  • Privacy Policy
  • Terms & Conditions
  • License
  • Refunds
  • Cookies

© 2026 ReUI. All rights reserved.

3.7K

Shadcn File Type

PreviousNext

Custom Shadcn File Type for React and Tailwind CSS. A shadcn file type icon in brand and family colors, outline, solid, light and mono variants, 6 sizes and resolution from a file name, path, URL or MIME type.

Base UIRadix UI
Radix UI

Installation

pnpm dlx shadcn@latest add @reui/file-type

Usage

import { FileType, getFileType } from "@/components/reui/file-type"

Shadcn File Type Free Components

Browse 14 production-ready Shadcn File Type components for dashboards, forms, and product UI. These examples follow the Radix UI implementation with accessible primitives from the Radix stack and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.

Browse all 14 Shadcn File Type components for copy-ready layouts, dashboards, and forms built with Tailwind CSS in the ReUI library.

Event CalendarFile Upload

On This Page

InstallationUsageFor AI AgentsExamplesOutline, solid, light and monoSizesFamily glyphsData grid file browserResolve from a file nameAPI ReferenceTypes and colorsFileTypeVariantsSizingLabel and glyphgetFileTypegetFileTypeCategoryCustomizationData AttributesAccessibility
<FileType type="pdf" /> <FileType type={getFileType("Q3 report.xlsx")} variant="solid" />

For AI Agents

Hand your agent this brief or the ReUI MCP, not a screenshot. Free to install.

When to use

  • Attachment lists, upload queues and file browser rows, each icon resolved from a name or MIME type
  • Export format pickers and legends of the formats a product accepts or produces
  • Version tags, document codes and marks such as a lock or star drawn on a document

When not to use

  • A drop zone or picker that collects documents before they are sent: use the File Upload component
  • Documents carried by a chat message as chips with idle, uploading and error states: use the Attachment component
  • A complete drive page with a folder tree, list and grid views and bulk actions: use the Files blocks

Agent notes

  • type takes an extension, not a file name: report.pdf passed straight in draws a neutral page labeled REPORT.PDF, so run names through getFileType
  • FileType is not focusable and has no keyboard behavior, so an action goes on a wrapping Button or link with its own accessible name
Markdownhttps://reui.io/docs/components/radix/file-type.md
MCPget_component("file-type")
Installnpx shadcn@latest add @reui/file-type
Full index for agents:llms.txtReUI MCP setup

Examples

Every built-in format by family, an attachment list, an upload queue, custom labels, custom colors and unknown types, an export format picker, custom icons and custom badges are on the File Type page.

Outline, solid, light and mono

import {
  FileType,
  type FileTypeName,
  type FileTypeVariant,
} from "@/components/reui/file-type"

const variants: FileTypeVariant[] = ["outline", "solid", "light", "mono"]

// The same types in every row, so the variant is the only thing that changes.
const types = [
  "pdf",
  "docx",
  "xlsx",
  "jpg",
  "zip",
] as const satisfies readonly FileTypeName[]

export function Pattern() {
  return (
    <div className="mx-auto flex w-fit flex-col gap-5">
      {variants.map((variant) => (
        <div key={variant} className="flex flex-col gap-2.5">
          <span className="text-muted-foreground text-xs capitalize">
            {variant}
          </span>
          <div className="flex gap-4">
            {types.map((type) => (
              <FileType key={type} type={type} variant={variant} />
            ))}
          </div>
        </div>
      ))}
    </div>
  )
}

Sizes

import {
  FileType,
  type FileTypeProps,
} from "@/components/reui/file-type"

const sizes = [
  { size: "mini", px: 18 },
  { size: "xs", px: 24 },
  { size: "sm", px: 32 },
  { size: "default", px: 40 },
  { size: "lg", px: 48 },
  { size: "xl", px: 56 },
] as const satisfies readonly { size: FileTypeProps["size"]; px: number }[]

export function Pattern() {
  // Every caption is 2 lines, so `items-end` lines up the icons' bottoms.
  return (
    <div className="mx-auto flex w-fit items-end gap-6">
      {sizes.map(({ size, px }) => (
        <div key={size} className="flex flex-col items-center gap-3">
          <FileType type="pdf" size={size} />
          {/* A zero-width caption centers on the icon without widening its
              column, so the gaps between icons are equal, not between
              captions ("default" is wider than its icon). */}
          <span className="flex w-0 flex-col items-center text-xs whitespace-nowrap">
            <span className="font-medium">{size}</span>
            <span className="text-muted-foreground tabular-nums">{px}px</span>
          </span>
        </div>
      ))}
    </div>
  )
}

Family glyphs

import {
  FILE_TYPE_CATEGORIES,
  FileType,
} from "@/components/reui/file-type"

// The blank page goes last: it stands for a file with no type, so it closes
// the set instead of sitting between two families.
const families = [
  ...FILE_TYPE_CATEGORIES.filter((family) => family !== "empty"),
  "empty",
] as const

export function Pattern() {
  // Cells fit "Presentation" in a monospace font. The width cap holds a row
  // to 5, so with room the 13 families wrap as 5, 5 and a centered 3.
  return (
    <ul className="mx-auto flex max-w-lg flex-wrap justify-center gap-x-1 gap-y-5">
      {families.map((family) => (
        <li key={family} className="flex w-22 flex-col items-center gap-2">
          {/* The caption names the family, so the icon would read twice. */}
          <FileType type={family} aria-hidden="true" />
          <span className="text-muted-foreground text-xs capitalize">
            {family}
          </span>
        </li>
      ))}
    </ul>
  )
}

Data grid file browser

Resolve from a file name

API Reference

FileType renders a single span with role="img". Inside it, an inline SVG draws the page, its folded corner and the family glyph, and an HTML band in front of the page carries the label (in solid, the label sits on the page with no band). There are no sub-components and no Base UI or Radix part, so both bases install the same file, and the only dependencies are class-variance-authority and cn. It has no state, no hooks and no "use client" directive, so it renders in a Server Component, and the helpers run on the server or the client.

Types and colors

type takes 1 of 99 built-in extensions, 1 of 13 family names, or any other string. It ignores case, surrounding spaces and leading dots, so "PDF", ".pdf" and "pdf" render the same icon.

  • An extension shows its label in upper case, in its color.
  • A family name shows the family glyph and no label, in the family color.
  • "empty", or no type, shows a blank page.
  • Any other string, such as "dwg", shows a neutral page (text-zinc-500) labelled with the string in upper case. Its family is file, which has no glyph.
FamilyExtensionsColorAccessible name
document9text-blue-600Document
spreadsheet6text-green-600Spreadsheet
presentation4text-red-600Presentation
image13text-violet-600Image
video8text-blue-600Video
audio7text-pink-600Audio file
code26text-blue-600Code file
archive7text-zinc-500Archive
design8text-purple-600Design file
executable7text-zinc-500Application
font4text-zinc-500Font file
folder0text-violet-600Folder
empty0text-zinc-500File

The family color paints a family type's glyph, and an extension without a color of its own.

A format takes its brand's color (Word blue, Excel green, PowerPoint red, the GitHub language colors for code), else its family's color, and a format with neither, such as an archive, plain text, data, an installer or a font, is neutral (text-zinc-500). The mono variant sets every type in the theme gray instead. Every brand and family color is the 600 step, so the set reads as one palette and the white label text holds 3:1 or better on all of them. Yellow (png, svg and JavaScript) is the exception: white text on it stays under 3:1 until the 700 step, so it is text-yellow-500 with dark label text, as the JavaScript logo is.

ExtensionsFamilyColor
pdfdocumenttext-orange-600
doc, docxdocumenttext-blue-600
odtdocumenttext-blue-600
rtfdocumenttext-blue-600
pagesdocumenttext-amber-600
txtdocumenttext-zinc-500
mddocumenttext-zinc-500
logdocumenttext-zinc-500
xls, xlsx, xlsmspreadsheettext-green-600
odsspreadsheettext-green-600
csvspreadsheettext-green-600
tsvspreadsheettext-green-600
ppt, pptxpresentationtext-red-600
odppresentationtext-red-600
keypresentationtext-blue-600
pngimagetext-yellow-500
jpg, jpegimagetext-violet-600
gifimagetext-violet-600
webpimagetext-violet-600
avifimagetext-violet-600
heicimagetext-violet-600
bmpimagetext-violet-600
tif, tiffimagetext-violet-600
icoimagetext-violet-600
rawimagetext-zinc-500
svgimagetext-yellow-500
mp4videotext-blue-600
movvideotext-blue-600
avivideotext-blue-600
mkvvideotext-blue-600
webmvideotext-blue-600
mpeg, mpgvideotext-blue-600
wmvvideotext-blue-600
mp3audiotext-pink-600
wavaudiotext-pink-600
aacaudiotext-pink-600
flacaudiotext-pink-600
oggaudiotext-pink-600
m4aaudiotext-pink-600
aiffaudiotext-pink-600
js, mjscodetext-yellow-500
jsxcodetext-yellow-500
tscodetext-blue-600
tsxcodetext-blue-600
html, htmcodetext-orange-600
csscodetext-purple-600
scsscodetext-pink-600
jsoncodetext-zinc-500
xmlcodetext-zinc-500
yaml, ymlcodetext-zinc-500
sqlcodetext-zinc-500
pycodetext-blue-600
javacodetext-orange-600
phpcodetext-indigo-600
rbcodetext-red-600
gocodetext-cyan-600
rscodetext-orange-600
ccodetext-zinc-500
cppcodetext-blue-600
hcodetext-zinc-500
cscodetext-purple-600
shcodetext-green-600
rsscodetext-orange-600
ziparchivetext-zinc-500
rararchivetext-zinc-500
7zarchivetext-zinc-500
tararchivetext-zinc-500
gz, tgzarchivetext-zinc-500
bz2archivetext-zinc-500
figdesigntext-purple-600
sketchdesigntext-amber-600
psddesigntext-sky-600
aidesigntext-orange-600
epsdesigntext-purple-600
indddesigntext-pink-600
aepdesigntext-violet-600
xddesigntext-fuchsia-600
exeexecutabletext-zinc-500
msiexecutabletext-zinc-500
dmgexecutabletext-zinc-500
apkexecutabletext-green-600
debexecutabletext-rose-600
isoexecutabletext-zinc-500
appexecutabletext-zinc-500
ttffonttext-zinc-500
otffonttext-zinc-500
woff, woff2fonttext-zinc-500

Both lists are exported: FILE_TYPE_EXTENSION_NAMES holds the 99 extensions and FILE_TYPE_CATEGORIES the 13 families. FileTypeName is the union of both, and type accepts any string beside it, so an editor still autocompletes the built-in names.

FileType

PropTypeDefaultDescription
typeFileTypeName | (string & {})-An extension, a family, "empty", or any other string for a neutral labelled page. Unset shows a blank page. getFileType resolves a file name.
variant"outline" | "solid" | "light" | "mono""outline"How the page is painted. See Variants.
size"mini" | "xs" | "sm" | "default" | "lg" | "xl""default"A preset box, 18px to 56px. A Tailwind size-* class in className overrides it.
labelReactNode-Replaces the extension text, for a version tag or a document code. null or "" hides it. See Label and glyph.
glyphbooleanfalseShows the family glyph instead of the label.
iconReactNode-A custom icon drawn on the page in place of the glyph and the label, in the glyph's color. See Label and glyph.
labelClassNamestring-Classes for the label badge, merged last. See Customization.
classNamestring-A text-* class recolors the type, and a size-* class resizes it. See Customization.

All span props are supported, except children: the component takes none.

Variants

VariantPageLabel badgeGlyph
outlineTransparent, with a grey outline and foldThe type color, --file-type-foreground textThe type color, lifted 30% toward white in dark mode
solidFilled with the type color, with a white 30% corner and a lighter foldNo badge: the text sits on the page in the glyph's color, white by default--file-type-foreground, white by default
lightA soft grey muted fill, with the grey outline and foldThe type color, --file-type-foreground textThe type color, lifted 30% toward white in dark mode
monoTransparent, with the grey outline and foldThe theme's muted-foreground gray for every type, with text in the page colorThe same gray, with no dark-mode lift

--file-type-foreground is white by default (the page color in mono), and the grey outline is --file-type-border. In dark mode the outline and light glyph mixes 30% white into the type color so a 1px line stays visible on a dark page, while the badge keeps the shade its white text needs. A neutral type fills its solid page 55% lighter (--file-type-fill) and draws its glyph and label in dark gray (--file-type-glyph), so it does not read as a dark slab next to the colored ones.

mono ignores the type color and draws every type in the theme's muted-foreground, for a list where color would compete with the content. A text-* class in className still recolors it.

<FileType type="xlsx" variant="solid" />

Sizing

Every size draws the same design: the page and the label band with its text. The root is an inline-size container and everything inside is measured in cqw against its width, so the band, its text, padding and corners and the line all scale with the box.

SizeBoxLabel textLabel band heightLine
mini18px5.8px8.1px1px
xs24px5.8px8.4px1px
sm32px7.7px11.2px1px
default40px9.6px14px1px
lg48px11.5px16.8px1.2px
xl56px13.4px19.6px1.4px

The band has the same geometry on every type: 45% from the top, 5% from the left, 35% of the box high and at most 95% of it wide, with 7% padding on each side, 10.5% in Maia, Luma and Rhea so the pill's round ends clear the text. Its text is 24% of the width, so its capitals fill half the band, and shrinks only when the characters would run past the band: at 40px WOFF2 renders at 9px and SKETCH at 7.5px (8.2px and 6.9px with the pill's padding). The capitals are centered on their own height rather than the font's line, and the band moves by up to half a pixel so that browsers, which draw text on whole pixels, keep them centered. The line is 1px up to 40px, then 2.5% of the width.

In solid there is no band: the text sits on the page, two thirds of the box wide, and every label up to 4 characters is 20.5% of the width (8.2px at 40px), so a row of mixed types reads even. A longer label shrinks to fit the page: at 40px WOFF2 renders at 6.6px and SKETCH at 5.5px. Up to 20px each label fits the page on its own, semibold: PDF at 4.4px and DOCX at 3.3px at 18px.

Up to 20px the icon is tuned to stay legible in a table row: the band is 45% of the box from 40% down, its text is semibold at a third of the box (5.8px at 18px), the outline is darker, and a glyph is drawn at full size instead of 75%.

For a size off the scale, put a plain Tailwind size class in className. It replaces the preset box, and the label and the line scale with it.

<FileType type="pdf" className="size-20" />
<FileType type="pdf" className="size-[72px]" />
<div className="size-24">
  <FileType type="pdf" className="size-full" />
</div>

fileTypeVariants({ size }) returns the root's classes for a size, for a wrapper that needs to match them.

Label and glyph

The label band is HTML text, the extension in upper case by default. label replaces it with any node, such as a version tag ("V3") or a document code ("CA-1042"). A string longer than 4 characters shrinks to fit the band, so keep it short. A label that is not a string is sized as 4 characters and truncated if it runs past the band.

<FileType type="docx" label="V3" />
<FileType type="empty" variant="light" label="CA-1042" />
<FileType type="pdf" label={null} />
<FileType type="pdf" glyph />
  • A string label joins the accessible name: type="docx" label="V3" reads "V3, DOCX file", and type="empty" label="CA-1042" reads "CA-1042, File". A label that only repeats the extension is not read twice, and a label that is not a string leaves the name as it is.
  • label={null} or label="" hides the band. The family glyph shows in its place when the type has one; an unknown type or "empty" shows a blank page.
  • glyph shows the family glyph instead of the label at any size. A family type, or a type with no label text, shows its glyph without it.

There are 12 glyphs, one per family except empty: document, spreadsheet, presentation, image, video, audio, code, archive, design, executable, font and folder. They share one 10-unit box under the fold on the 24-unit page grid, drawn at 75% of it, and draw with the page's line, so every family sits at the same weight. The document glyph is 3 uneven, left-aligned lines.

icon draws any icon on the page in place of the glyph and the label, in the glyph's color: the type color in outline and light, the foreground in solid, the theme gray in mono. It is sized like a glyph and decorative, so the type still names the icon; pass an aria-label when the icon adds meaning ("Locked PDF file").

<FileType type="pdf" icon={<LockIcon />} aria-label="Locked PDF file" />

getFileType

Resolves a file name, path, URL, bare extension, family name or MIME type to a type, always in lower case. It returns FileTypeName | (string & {}). A URL's query and hash are dropped first. Outside a URL a ? or # can be part of the name, so "invoice #12.pdf" is a PDF, and they start a query only when the value does not already end in an extension or a slash ("/uploads/scan.png?v=3").

InputReturnsRule
"Q3 report.pdf""pdf"The text after the last dot.
"photos/IMG_2041.HEIC""heic"The last segment of a path, either slash, in any case.
"archive.tar.gz""gz"Only the last extension counts.
"https://example.com/data.csv?v=2""csv"A URL with a scheme is read by its path, so its host never counts as a file name.
"https://example.com""empty"A bare host, or a URL whose path is /, has no type.
"assets/""folder"A value ending in / or \ is a folder, a URL path included.
"pdf", ".pdf", "image""pdf", "pdf", "image"A bare extension or family resolves to itself.
"README", ".env""empty"Any other name with no extension has no type.
"plan.dwg""dwg"An unknown extension comes back as is and renders neutral.
"application/pdf""pdf"68 exact MIME types map first. See below.
"image/photo.png""png"A subtype with a dot whose last part is a built-in extension is a storage key, not a MIME type.
"image/webp", "font/woff2""webp", "woff2"A subtype that is a built-in extension in the same family. Under application/ and text/, any built-in extension.
"audio/webm", "video/ogg""audio", "video"A subtype from another family does not count: WebM is a video format, so audio/webm is audio.
"video/x-flv""video"Any other image/, video/, audio/ or font/ type is its family.
"text/plain; charset=utf-8""txt"Parameters are ignored, and any other text/ type is txt.
"application/octet-stream""empty"Any other MIME type has no type.

The exact MIME map covers the types whose subtype does not name the extension: PDF, Word, Excel (macro-enabled included), PowerPoint, OpenDocument, RTF and Keynote; ZIP, RAR, 7-Zip, tar, gzip and bzip2, with their x- forms (application/x-gzip, application/x-rar-compressed, application/x-bzip2); JSON, XML, YAML, RSS, JavaScript, PHP and shell scripts; Windows, Android and Debian packages, Apple disk images and ISO images; TTF and WOFF fonts; Photoshop, PostScript, JPEG, SVG and icons; QuickTime, AVI, Matroska and WMV; MP3, WAV, M4A, FLAC and AIFF; and plain text, Markdown, Python, C, CSV, TSV, HTML and CSS.

import { FileType, getFileType } from "@/components/reui/file-type"
 
function AttachmentRow({ file }: { file: File }) {
  return (
    <div className="flex items-center gap-2">
      <FileType type={getFileType(file.name)} size="mini" aria-hidden="true" />
      <span className="truncate">{file.name}</span>
    </div>
  )
}

A browser File also carries a MIME type in file.type, which can be empty or a generic application/octet-stream, so the name is usually the better input. A folder name is not a file name: "v2.1 exports" would resolve to a "1 exports" extension, so pass "folder" for folders yourself.

getFileTypeCategory

Returns the family of a type, for grouping or filtering a file list: 1 of the 13 families, "file" for an unknown type, or "empty" for none. The return type, FileTypeCategory, is those 14 values. It takes a type, not a file name, so resolve the name with getFileType first.

getFileTypeCategory("xlsx") // "spreadsheet"
getFileTypeCategory(getFileType("Q3 report.pdf")) // "document"
getFileTypeCategory("dwg") // "file"
getFileTypeCategory() // "empty"

Customization

The type color is the root's text color, and the label band, the glyph and the solid fill all follow it, so a single text-* class in className replaces the type's color. For white label text at 4.5:1, every Tailwind 700 shade works. At 600, orange, amber, yellow, lime, green, emerald, teal, cyan and sky fall short, and most 500 shades are too light. For a light color, set --file-type-foreground to a dark one instead.

<FileType type="xlsx" className="text-green-700" />
<FileType type="pdf" className="text-lime-400 [--file-type-foreground:black]" />
<FileType
  type="sketch"
  variant="solid"
  className="text-amber-400 [--file-type-foreground:black]"
/>

The variables below are read from the root, and a class in className replaces any of them.

VariableDefaultDescription
--file-type-size--spacing(10), from sizeBox width and height. A size-* class replaces the box instead.
--file-type-strokeUnset, which means max(1px, 2.5cqw)Line width of the page, the fold and the glyph.
--file-type-borderUnset, which means muted-foreground at 40%, 65% up to 20pxPage outline and fold in outline, light and mono.
--file-type-fillUnset, which means the type color; 55% lighter on neutral typesThe page in solid.
--file-type-glyphUnset, which means --file-type-foreground; dark gray on neutral typesThe glyph, the label and a custom icon in solid.
--file-type-foregroundwhite, the page color in monoLabel text, and the glyph in solid.
--file-type-radiusUnset, which means the style's label radiusLabel band corners.

The label band's corners follow the installed style in the ReUI Badge's proportions: 10cqw (4px at 40px) in Vega, Nova and Mira, a pill in Maia, Luma and Rhea, and square in Lyra and Sera. --file-type-radius sets them for every style. In Lyra and Sera the page, its folded corner and the solid flap are square too, so the whole icon follows the style.

labelClassName is merged last onto the badge, so any of its styles can be replaced: background, radius, padding, font size and weight, or position. The text is a child of the badge, so recolor it with *:text-*.

<FileType type="pdf" labelClassName="rounded-full" />
<FileType type="docx" label="DRAFT" labelClassName="bg-amber-400 *:text-amber-950" />
<FileType type="xlsx" labelClassName="bg-foreground *:text-background" />
<FileType type="pdf" className="[--file-type-radius:9999px]" />
<FileType
  type="pdf"
  className="[--file-type-border:var(--border)] [--file-type-stroke:1.5px]"
/>

Data Attributes

AttributeOnValues
data-typerootThe type in lower case without a leading dot: pdf, image, empty, dwg
data-categoryrootThe family, file for an unknown type, empty for none
data-variantrootoutline, solid, light, mono
data-sizerootThe size prop, default when unset, even when a class resizes the box
Partdata-slot
The rootfile-type
The SVGfile-type-icon
The glyphfile-type-glyph, a group in the SVG, when the glyph shows and the family has one
The iconfile-type-custom-icon, a span over the page, when icon is set
The labelfile-type-label, when there is label text and glyph is not set

Accessibility

  • The root is an image named after its type: "PDF file" for an extension, unknown ones included ("DWG file"), the family's name for a family type (see Types and colors), and "File" when there is no type. The SVG inside is hidden.
  • A string label joins the name ("V3, DOCX file"). Pass aria-label to name the icon yourself; it replaces the generated one.
  • Pass aria-hidden="true" when the text next to the icon already names the type, such as a file name in a list row or a family caption, so it is not read twice.
  • The label text is small, so WCAG AA asks 4.5:1 for it. In outline and light, 80 of the 99 extensions meet that, and every one has 3:1 or better. The 600 shades of orange, amber, green and cyan fall between 3:1 and 4.5:1 with white text: pdf, html, htm, java, rs, rss and ai (3.6:1), go (3.6:1), pages and sketch (3.2:1), the spreadsheets, sh and apk (3.2:1), and psd (4.0:1). solid puts the same white text on the type color, so the same ratios hold, except the 27 neutral types: their dark gray label on the lighter page is 4.1:1. Yellow types take dark text, 7.6:1. The ratios are for Tailwind's default palette. In mono the label is the page color on muted-foreground: 4.7:1 in light mode and 7.6:1 in dark with the default neutral theme.
  • To raise the contrast, use a darker text-* class (text-green-700 on xlsx reaches 4.9:1). See Customization.
  • A glyph is a line in the type color, so a yellow png or svg glyph sits under 3:1 on a white page. In dark mode the outline and light glyph is lifted toward white to stay visible on a dark page.
  • At mini and xs the label text is 5.8px, so keep the file name or the extension in text next to the icon.
  • The icon is not focusable and has no keyboard behavior. Wrap it in a link or a button for an action, and give that control its own name.
import {
  FileType,
  type FileTypeName,
} from "@/components/reui/file-type"

const formats = [
  "pdf",
  "docx",
  "xlsx",
  "pptx",
  "jpg",
  "mp4",
  "mp3",
  "zip",
  "fig",
  "html",
] as const satisfies readonly FileTypeName[]

export function Pattern() {
  // No captions: each icon names itself ("PDF file", "MP4 file"), so a screen
  // reader still hears every format.
  return (
    <div className="mx-auto grid w-fit grid-cols-5 gap-x-4 gap-y-6">
      {formats.map((type) => (
        <FileType key={type} type={type} />
      ))}
    </div>
  )
}
"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  FileType,
  getFileType,
  getFileTypeCategory,
  type FileTypeCategory,
} from "@/components/reui/file-type"
import { useTable } from "@tanstack/react-table"
import type { ColumnDef, SortingState } from "@tanstack/react-table"

interface FileItem {
  id: string
  name: string
  kind: "file" | "folder"
  /** Bytes. Files only: a folder has an item count instead. */
  size?: number
  items?: number
  /** ISO date. */
  modified: string
  owner: string
}

const files: FileItem[] = [
  {
    id: "1",
    name: "Brand assets",
    kind: "folder",
    items: 24,
    modified: "2026-10-02",
    owner: "Ava Patel",
  },
  {
    id: "2",
    name: "Legal",
    kind: "folder",
    items: 6,
    modified: "2026-09-21",
    owner: "Marco Silva",
  },
  {
    id: "3",
    name: "Q3 financial report.pdf",
    kind: "file",
    size: 2516582,
    modified: "2026-10-05",
    owner: "Marco Silva",
  },
  {
    id: "4",
    name: "Customer interviews - synthesis and next steps.docx",
    kind: "file",
    size: 190464,
    modified: "2026-10-06",
    owner: "You",
  },
  {
    id: "5",
    name: "Revenue forecast 2027.xlsx",
    kind: "file",
    size: 933888,
    modified: "2026-10-01",
    owner: "Lena Fischer",
  },
  {
    id: "6",
    name: "Waitlist export.csv",
    kind: "file",
    size: 5033165,
    modified: "2026-09-29",
    owner: "You",
  },
  {
    id: "7",
    name: "Launch keynote.pptx",
    kind: "file",
    size: 19503513,
    modified: "2026-10-04",
    owner: "Ava Patel",
  },
  {
    id: "8",
    name: "Homepage hero.png",
    kind: "file",
    size: 1363149,
    modified: "2026-09-30",
    owner: "Ava Patel",
  },
  {
    id: "9",
    name: "Team offsite.jpg",
    kind: "file",
    size: 3250586,
    modified: "2026-09-12",
    owner: "Lena Fischer",
  },
  {
    id: "10",
    name: "Product walkthrough.mp4",
    kind: "file",
    size: 149631795,
    modified: "2026-10-03",
    owner: "Noah Kim",
  },
  {
    id: "11",
    name: "Podcast episode 12.mp3",
    kind: "file",
    size: 40055603,
    modified: "2026-09-24",
    owner: "Noah Kim",
  },
  {
    id: "12",
    name: "Press kit.zip",
    kind: "file",
    size: 59139686,
    modified: "2026-10-02",
    owner: "Ava Patel",
  },
  {
    id: "13",
    name: "Design system.fig",
    kind: "file",
    size: 24012390,
    modified: "2026-10-06",
    owner: "Ava Patel",
  },
  {
    id: "14",
    name: "app.config.json",
    kind: "file",
    size: 4301,
    modified: "2026-10-06",
    owner: "Noah Kim",
  },
  {
    id: "15",
    name: "Office floor plan.dwg",
    kind: "file",
    size: 7864320,
    modified: "2026-08-27",
    owner: "Lena Fischer",
  },
]

const FAMILY_LABELS: Record<Exclude<FileTypeCategory, "file">, string> = {
  archive: "Archive",
  audio: "Audio",
  code: "Code",
  design: "Design",
  document: "Document",
  empty: "File",
  executable: "Application",
  folder: "Folder",
  font: "Font",
  image: "Image",
  presentation: "Presentation",
  spreadsheet: "Spreadsheet",
  video: "Video",
}

// A folder name is not a file name: "v2.1 exports" would resolve to a
// "1 exports" extension, so folders never go through getFileType.
function fileTypeOf(file: FileItem) {
  return file.kind === "folder" ? "folder" : getFileType(file.name)
}

function kindOf(file: FileItem) {
  const type = fileTypeOf(file)
  const category = getFileTypeCategory(type)
  // An unknown extension has no family to name, so it names the kind itself.
  return category === "file"
    ? `${type.toUpperCase()} file`
    : FAMILY_LABELS[category]
}

const UNITS = ["B", "KB", "MB", "GB"]
const sizeNumber = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 })

function formatBytes(bytes: number) {
  let value = bytes
  let unit = 0
  while (value >= 1024 && unit < UNITS.length - 1) {
    value /= 1024
    unit += 1
  }
  return `${sizeNumber.format(value)} ${UNITS[unit]}`
}

// A date-only ISO string parses as UTC midnight, so it is formatted in UTC:
// a local zone west of it would show the previous day, and differ from the
// server render.
const dateFormat = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  year: "numeric",
  timeZone: "UTC",
})

// A fixed locale keeps the order identical on the server and the client, and
// numeric puts "episode 2" before "episode 12".
const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" })

// Folders group ahead of files, as in a file manager. A descending sort
// reverses the whole order, so they then move to the end.
function compareByName(a: FileItem, b: FileItem) {
  if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1
  return collator.compare(a.name, b.name)
}

export function Pattern() {
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: false },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, FileItem>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: ({ column }) => (
          <DataGridColumnHeader title="Name" column={column} />
        ),
        cell: ({ row }) => {
          const file = row.original
          // The name already reads its extension, so a file's icon is
          // hidden. A folder name says nothing about what the row is, so
          // the folder icon keeps its "Folder" name.
          return (
            <div className="flex min-w-0 items-center gap-2">
              <FileType
                type={fileTypeOf(file)}
                size="mini"
                aria-hidden={file.kind === "folder" ? undefined : true}
              />
              <span className="truncate font-medium" title={file.name}>
                {file.name}
              </span>
            </div>
          )
        },
        sortFn: (a, b) => compareByName(a.original, b.original),
        size: 300,
      },
      {
        id: "type",
        accessorFn: kindOf,
        header: ({ column }) => (
          <DataGridColumnHeader title="Type" column={column} />
        ),
        size: 130,
      },
      {
        accessorKey: "size",
        header: ({ column }) => (
          <DataGridColumnHeader title="Size" column={column} />
        ),
        cell: ({ row }) => {
          const { size, items } = row.original
          return size === undefined ? (
            <span className="text-muted-foreground">{items} items</span>
          ) : (
            formatBytes(size)
          )
        },
        // Folders have no byte size, so they sit last in both directions.
        sortFn: "basic",
        sortUndefined: "last",
        size: 100,
        // The header is a padded button in a flex row: justify-end moves it
        // to the end, and -me-2 mirrors the header's own -ms-2, so the sort
        // arrow lines up with the figures below.
        meta: {
          headerClassName: "text-end [&>div]:-me-2 [&>div]:justify-end",
          cellClassName: "text-end tabular-nums",
        },
      },
      {
        accessorKey: "modified",
        header: ({ column }) => (
          <DataGridColumnHeader title="Modified" column={column} />
        ),
        cell: ({ row }) => dateFormat.format(new Date(row.original.modified)),
        // ISO dates sort chronologically as plain text.
        sortFn: "text",
        size: 130,
      },
      {
        accessorKey: "owner",
        header: ({ column }) => (
          <DataGridColumnHeader title="Owner" column={column} />
        ),
        size: 140,
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    // The shared bundle registers a pagination row model; manualPagination
    // says the data is already the page, so every file renders.
    manualPagination: true,
    columns,
    data: files,
    getRowId: (row: FileItem) => row.id,
    state: { sorting },
    onSortingChange: setSorting,
  })

  return (
    <DataGrid table={table} recordCount={files.length}>
      <DataGridContainer>
        <DataGridScrollArea>
          <DataGridTable />
        </DataGridScrollArea>
      </DataGridContainer>
    </DataGrid>
  )
}
"use client"

import { useId, useRef, useState } from "react"
import {
  FileType,
  getFileType,
  getFileTypeCategory,
  type FileTypeVariant,
} from "@/components/reui/file-type"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { Field, FieldLabel } from "@/components/ui/field"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from "@/components/ui/input-group"
import { XIcon } from 'lucide-react'

// One sample per kind of input getFileType reads: a file name, a path in
// another case, a MIME type, a folder, a bare name and a URL with a query.
// The long URL goes last, so the wrap never strands a short chip on a row
// of its own.
const SAMPLES = [
  "Q3 report.pdf",
  "photos/IMG_2041.HEIC",
  "video/quicktime",
  "assets/",
  "README",
  "https://example.com/data.csv?v=2",
]

const VARIANTS: { variant: FileTypeVariant; label: string }[] = [
  { variant: "outline", label: "Outline" },
  { variant: "solid", label: "Solid" },
  { variant: "light", label: "Light" },
  { variant: "mono", label: "Mono" },
]

// Only the two fallbacks need words: a value with nothing to resolve draws a
// blank page, and an unknown extension draws a neutral page with its label.
function describe(value: string, type: string) {
  if (!value.trim()) return "Nothing to resolve yet, so it shows a blank page."
  if (type === "empty") {
    return "No extension or known MIME type, so it shows a blank page."
  }
  if (getFileTypeCategory(type) === "file") {
    return "Unknown extension, so it shows a neutral page with its label."
  }
  return null
}

export function Pattern() {
  const id = useId()
  const inputRef = useRef<HTMLInputElement>(null)
  // Opens on a sample so the card shows a resolved type; Clear reaches the
  // blank state.
  const [value, setValue] = useState("Q3 report.pdf")

  // getFileType is a pure string parse, so the icons, the readout and the
  // hint all derive from the raw value on each render, with no effect.
  const type = getFileType(value)
  const family = getFileTypeCategory(type)
  const hint = describe(value, type)

  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <div className="flex flex-col gap-3">
        <Field>
          <FieldLabel htmlFor={`${id}-value`}>
            File name, path, URL or MIME type
          </FieldLabel>
          <InputGroup>
            <InputGroupAddon>
              {/* The field already shows the name and the status below
                  announces the result, so this icon is decorative here. */}
              <FileType type={type} size="mini" aria-hidden="true" />
            </InputGroupAddon>
            <InputGroupInput
              ref={inputRef}
              id={`${id}-value`}
              value={value}
              onChange={(event) => setValue(event.target.value)}
              placeholder="invoice.docx or image/png"
              autoComplete="off"
              spellCheck={false}
            />
            {value ? (
              <InputGroupAddon align="inline-end">
                <InputGroupButton
                  size="icon-xs"
                  aria-label="Clear"
                  onClick={() => {
                    // The button unmounts once the field is empty, so focus
                    // goes back to the field instead of falling to the body.
                    setValue("")
                    inputRef.current?.focus()
                  }}
                >
                  <XIcon  aria-hidden="true" />
                </InputGroupButton>
              </InputGroupAddon>
            ) : null}
          </InputGroup>
        </Field>
        <div
          role="group"
          aria-labelledby={`${id}-samples`}
          className="flex flex-wrap items-center gap-1.5"
        >
          <span
            id={`${id}-samples`}
            className="text-muted-foreground me-1 text-xs"
          >
            Try
          </span>
          {SAMPLES.map((sample) => (
            <Button
              key={sample}
              variant="outline"
              size="xs"
              className="max-w-full"
              onClick={() => setValue(sample)}
            >
              {/* The span, not the button, truncates: a long URL then ends
                  in an ellipsis on a narrow card instead of overflowing. */}
              <span className="truncate">{sample}</span>
            </Button>
          ))}
        </div>
      </div>
      <Card>
        <CardContent>
          <div className="grid grid-cols-4 justify-items-center gap-4">
            {VARIANTS.map(({ variant, label }) => (
              <div key={variant} className="flex flex-col items-center gap-2">
                <FileType type={type} variant={variant} size="lg" />
                <span className="text-muted-foreground text-xs">{label}</span>
              </div>
            ))}
          </div>
        </CardContent>
        <CardFooter>
          {/* A polite live region holding text only: the icons sit outside
              it, so each change is read once as "Type pdf, Family document". */}
          <div role="status" className="flex min-w-0 flex-1 flex-col gap-1">
            <dl className="flex min-w-0 flex-wrap gap-x-6 gap-y-1">
              <div className="flex min-w-0 gap-2">
                <dt className="text-muted-foreground">Type</dt>
                <dd className="truncate font-mono">{type}</dd>
              </div>
              <div className="flex gap-2">
                <dt className="text-muted-foreground">Family</dt>
                <dd className="font-mono">{family}</dd>
              </div>
            </dl>
            {hint ? (
              <p className="text-muted-foreground text-xs">{hint}</p>
            ) : null}
          </div>
        </CardFooter>
      </Card>
    </div>
  )
}