File Trigger

PreviousNext

A file trigger allows a user to access the file system with any pressable component.

"use client"

import { useState } from "react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerDemo() {
  const [file, setFile] = useState<string | null>(null)

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        onSelect={(e) => {
          const files = e ? Array.from(e) : []
          const filenames = files.map((file) => file.name)
          setFile(filenames[0] || null)
        }}
      >
        <Button variant="outline">Select a file</Button>
      </FileTrigger>
      {file && (
        <p className="text-muted-foreground text-sm">Selected: {file}</p>
      )}
    </div>
  )
}

Installation

pnpm dlx shadcn@latest add file-trigger

Usage

import { FileTrigger } from "@/components/ui/file-trigger"
import { Button } from "@/components/ui/button"
<FileTrigger onSelect={(files) => console.log(files)}>
  <Button>Select a file</Button>
</FileTrigger>

Features

A file input can be created with an <input type="file"> element, but this supports limited styling options and may not integrate well with the overall design of a website or application. FileTrigger extends the functionality of the standard file input element by working with a pressable child such as a Button to create accessible file inputs that can be styled as needed.

  • Customizable – Works with any pressable React Aria component, and custom components built with usePress.
  • File type filtering – Accepts specific file types using MIME types.
  • Multiple files – Supports selecting multiple files at once.
  • Directory selection – Allows users to select entire directories.
  • Media capture – Supports opening device camera on mobile devices.

Examples

Default

"use client"

import { useState } from "react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerDemo() {
  const [file, setFile] = useState<string | null>(null)

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        onSelect={(e) => {
          const files = e ? Array.from(e) : []
          const filenames = files.map((file) => file.name)
          setFile(filenames[0] || null)
        }}
      >
        <Button variant="outline">Select a file</Button>
      </FileTrigger>
      {file && (
        <p className="text-muted-foreground text-sm">Selected: {file}</p>
      )}
    </div>
  )
}
"use client"
 
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"
 
export default function FileTriggerDemo() {
  const [file, setFile] = useState<string | null>(null)
 
  return (
    <>
      <FileTrigger
        onSelect={(e) => {
          const files = e ? Array.from(e) : []
          const filenames = files.map((file) => file.name)
          setFile(filenames[0] || null)
        }}
      >
        <Button variant="outline">Select a file</Button>
      </FileTrigger>
      {file && <p>Selected: {file}</p>}
    </>
  )
}

Multiple Files

"use client"

import { useState } from "react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerMultiple() {
  const [files, setFiles] = useState<string[]>([])

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        allowsMultiple
        onSelect={(e) => {
          const fileList = e ? Array.from(e) : []
          const filenames = fileList.map((file) => file.name)
          setFiles(filenames)
        }}
      >
        <Button variant="outline">Select multiple files</Button>
      </FileTrigger>
      {files.length > 0 && (
        <div className="text-muted-foreground text-sm">
          <p className="font-medium">Selected files ({files.length}):</p>
          <ul className="mt-2 space-y-1">
            {files.map((file, index) => (
              <li key={index} className="truncate">
                {file}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}
<FileTrigger
  allowsMultiple
  onSelect={(e) => {
    const files = e ? Array.from(e) : []
    console.log(files.map((file) => file.name))
  }}
>
  <Button variant="outline">Select multiple files</Button>
</FileTrigger>

Accepted File Types

"use client"

import { useState } from "react"
import { ImageIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerImage() {
  const [image, setImage] = useState<string | null>(null)

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        acceptedFileTypes={[
          "image/png",
          "image/jpeg",
          "image/jpg",
          "image/gif",
        ]}
        onSelect={(e) => {
          const files = e ? Array.from(e) : []
          if (files.length > 0) {
            setImage(files[0].name)
          }
        }}
      >
        <Button variant="outline">
          <ImageIcon />
          Select an image
        </Button>
      </FileTrigger>
      {image && (
        <p className="text-muted-foreground text-sm">Selected: {image}</p>
      )}
    </div>
  )
}
<FileTrigger
  acceptedFileTypes={["image/png", "image/jpeg", "image/jpg", "image/gif"]}
  onSelect={(e) => {
    const files = e ? Array.from(e) : []
    console.log(files[0])
  }}
>
  <Button variant="outline">
    <ImageIcon />
    Select an image
  </Button>
</FileTrigger>

You can specify file types using MIME types. Common examples include:

  • image/* - All image types
  • image/png, image/jpeg - Specific image formats
  • video/* - All video types
  • application/pdf - PDF files
  • .txt, .doc - File extensions

Directory Selection

"use client"

import { useState } from "react"
import { FolderIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerDirectory() {
  const [files, setFiles] = useState<string[]>([])

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        acceptDirectory
        onSelect={(e) => {
          if (e) {
            const fileList = Array.from(e).map((file) =>
              file.webkitRelativePath !== ""
                ? file.webkitRelativePath
                : file.name
            )
            setFiles(fileList)
          }
        }}
      >
        <Button variant="outline">
          <FolderIcon />
          Select a directory
        </Button>
      </FileTrigger>
      {files.length > 0 && (
        <div className="text-muted-foreground text-sm">
          <p className="font-medium">
            Directory contents ({files.length} files):
          </p>
          <ul className="mt-2 max-h-32 space-y-1 overflow-y-auto">
            {files.slice(0, 10).map((file, index) => (
              <li key={index} className="truncate text-xs">
                {file}
              </li>
            ))}
            {files.length > 10 && (
              <li className="text-xs italic">
                ...and {files.length - 10} more files
              </li>
            )}
          </ul>
        </div>
      )}
    </div>
  )
}
<FileTrigger
  acceptDirectory
  onSelect={(e) => {
    if (e) {
      const fileList = Array.from(e).map((file) =>
        file.webkitRelativePath !== ""
          ? file.webkitRelativePath
          : file.name
      )
      console.log(fileList)
    }
  }}
>
  <Button variant="outline">
    <FolderIcon />
    Select a directory
  </Button>
</FileTrigger>

This reflects the webkitdirectory HTML attribute and allows users to select directories and their contents. Please note that support for this feature varies from browser to browser.

Media Capture

"use client"

import { useState } from "react"
import { CameraIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { FileTrigger } from "@/components/ui/file-trigger"

export function FileTriggerCamera() {
  const [image, setImage] = useState<string | null>(null)

  return (
    <div className="flex flex-col gap-4">
      <FileTrigger
        defaultCamera="environment"
        acceptedFileTypes={["image/*"]}
        onSelect={(e) => {
          const files = e ? Array.from(e) : []
          if (files.length > 0) {
            setImage(files[0].name)
          }
        }}
      >
        <Button variant="outline">
          <CameraIcon />
          Open Camera
        </Button>
      </FileTrigger>
      {image && (
        <p className="text-muted-foreground text-sm">Captured: {image}</p>
      )}
    </div>
  )
}
<FileTrigger
  defaultCamera="environment"
  acceptedFileTypes={["image/*"]}
  onSelect={(e) => {
    const files = e ? Array.from(e) : []
    console.log(files[0])
  }}
>
  <Button variant="outline">
    <CameraIcon />
    Open Camera
  </Button>
</FileTrigger>

To specify the media capture mechanism to capture media on the spot, pass user for the user-facing camera or environment for the outward-facing camera via the defaultCamera prop.

This behavior only works on mobile devices. On desktop devices, it will open the file system like normal.

API Reference

FileTrigger

The FileTrigger component wraps around a pressable child and includes a visually hidden input element that allows the user to select files from their device.

PropTypeDefaultDescription
acceptedFileTypesstring[]-Specifies what MIME types of files are allowed.
allowsMultiplebooleanfalseWhether multiple files can be selected.
defaultCamera"user" | "environment"-Specifies the use of a media capture mechanism to capture the media on the spot.
acceptDirectorybooleanfalseEnables the selection of directories instead of individual files.
onSelect(files: FileList | null) => void-Handler called when a user selects files. Receives a FileList object or null if cancelled.

If a visual label is not provided on the pressable child, then an aria-label or aria-labelledby prop must be passed to identify the file trigger to assistive technology.

Apart from the props above, the FileTrigger component also supports the props from the react-aria-components FileTrigger component. Check the React Aria documentation for more details.