Forms

File Trigger

A button that opens the native file picker, built on react-aria-components and styled with the quebi design system. Supports multiple files, directories, camera capture, and a pending state.

formfileuploadinputbutton

Default

Opens the native file picker. Defaults to the outline intent.

Intents

Mirrors the quebi Button intents.

Multiple files

Allow selecting more than one file at once.

Directory & camera

Swaps the glyph based on what the picker accepts.

Custom label

Pass children to override the default label.

Pending

Shows the quebi Loader while a file is processing.

Disabled

Controlled selection

Read the selected files via onSelect.

No file selected

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import {
  FileTrigger as FileTriggerPrimitive,
  type FileTriggerProps as FileTriggerPrimitiveProps,
} from "react-aria-components"
import type { VariantProps } from "tailwind-variants"
import { Button, type buttonStyles } from "@/components/button"
import { Loader } from "@/components/loader"

/**
 * FileTrigger — quebi design system
 *
 * A button that opens the native file picker, built on
 * react-aria-components' FileTrigger. It mirrors the quebi Button API
 * (intent / size / isCircle) and swaps its glyph based on intent:
 * camera for `defaultCamera`, folder for `acceptDirectory`, otherwise a
 * paperclip. While `isPending` it shows the quebi Loader.
 */
const PaperClipIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg
    data-slot="icon"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.8"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="M18.4 8.6 9.2 17.8a3.5 3.5 0 0 1-4.95-4.95l9.2-9.2a2.33 2.33 0 0 1 3.3 3.3l-9.21 9.2a1.17 1.17 0 0 1-1.65-1.65l8.5-8.5" />
  </svg>
)

const FolderIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg
    data-slot="icon"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.8"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
  </svg>
)

const CameraIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg
    data-slot="icon"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.8"
    strokeLinecap="round"
    strokeLinejoin="round"
    aria-hidden="true"
    {...props}
  >
    <path d="M3 8a2 2 0 0 1 2-2h2l1.5-2h7L17 6h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
    <circle cx="12" cy="12.5" r="3.2" />
  </svg>
)

export interface FileTriggerProps
  extends FileTriggerPrimitiveProps,
    VariantProps<typeof buttonStyles> {
  isDisabled?: boolean
  isPending?: boolean
  ref?: React.RefObject<HTMLInputElement>
  className?: string
}

export function FileTrigger({
  intent = "outline",
  size = "md",
  isCircle = false,
  ref,
  className,
  ...props
}: FileTriggerProps) {
  return (
    <FileTriggerPrimitive ref={ref} {...props}>
      <Button
        className={className}
        isDisabled={props.isDisabled}
        intent={intent}
        size={size}
        isCircle={isCircle}
      >
        {!props.isPending ? (
          props.defaultCamera ? (
            <CameraIcon />
          ) : props.acceptDirectory ? (
            <FolderIcon />
          ) : (
            <PaperClipIcon />
          )
        ) : (
          <Loader />
        )}
        {props.children ? (
          props.children
        ) : (
          <>
            {props.allowsMultiple
              ? "Browse files"
              : props.acceptDirectory
                ? "Browse"
                : "Browse a file"}
            ...
          </>
        )}
      </Button>
    </FileTriggerPrimitive>
  )
}