component — files

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.

  • form
  • file
  • upload
  • input
  • button

08 examples

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

file-trigger.tsx

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.
 *
 * It defaults to `size="sm"`, not Button's `md`. A picker is read as a field
 * control, not as a call to action: `md` is the design's 46px button, while
 * every field-shaped control in the library — Input, the Select trigger, the
 * DropZone label — is `text-sm` at `py-2` by default, and a FileTrigger sits
 * directly against those (inside a DropZone in `conform-file-trigger`, beside
 * an Input in a form). `sm` shares that type and vertical padding, so the two
 * share a baseline. A consumer who wants the CTA scale passes `size="md"`.
 */
const PaperClipIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg
    data-slot="icon"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
    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.5"
    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.5"
    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 = "sm",
  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>
  )
}