Installation
npx @park-ui/cli add file-uploadAdd Component
Copy the code snippet below into you components folder.
'use client'
import { FileUpload, useFileUploadContext } from '@ark-ui/react/file-upload'
import { FileIcon, XIcon } from 'lucide-react'
import { type ComponentProps, forwardRef, useMemo } from 'react'
import { createStyleContext, type HTMLStyledProps, Stack } from 'styled-system/jsx'
import { fileUpload } from 'styled-system/recipes'
import { Span } from '@/components/ui'
const { withProvider, withContext } = createStyleContext(fileUpload)
export type RootProps = ComponentProps<typeof Root>
export type ItemProps = ComponentProps<typeof Item>
export const Root = withProvider(FileUpload.Root, 'root')
export const RootProvider = withProvider(FileUpload.RootProvider, 'root')
export const ClearTrigger = withContext(FileUpload.ClearTrigger, 'clearTrigger')
export const Dropzone = withContext(FileUpload.Dropzone, 'dropzone')
export const HiddenInput = FileUpload.HiddenInput
export const Item = withContext(FileUpload.Item, 'item')
export const ItemDeleteTrigger = withContext(FileUpload.ItemDeleteTrigger, 'itemDeleteTrigger', {
defaultProps: { children: <XIcon /> },
})
export const ItemGroup = withContext(FileUpload.ItemGroup, 'itemGroup')
export const ItemName = withContext(FileUpload.ItemName, 'itemName')
export const ItemPreview = withContext(FileUpload.ItemPreview, 'itemPreview', {
defaultProps: {
children: <FileIcon />,
},
})
export const ItemPreviewImage = withContext(FileUpload.ItemPreviewImage, 'itemPreviewImage')
export const ItemSizeText = withContext(FileUpload.ItemSizeText, 'itemSizeText')
export const Label = withContext(FileUpload.Label, 'label')
export const Trigger = withContext(FileUpload.Trigger, 'trigger')
export { FileUploadContext as Context } from '@ark-ui/react/file-upload'
interface ItemsBaseProps {
showSize?: boolean | undefined
clearable?: boolean | undefined
files?: File[] | undefined
}
interface ItemsProps extends Omit<ItemProps, 'file'>, ItemsBaseProps {}
export const Items = (props: ItemsProps) => {
const { showSize, clearable, files, ...rest } = props
const fileUpload = useFileUploadContext()
const acceptedFiles = files ?? fileUpload.acceptedFiles
return acceptedFiles.map((file) => (
<Item file={file} key={file.name} {...rest}>
<ItemPreview />
<Stack gap="0.5" flex="1">
<ItemName />
{showSize && <ItemSizeText />}
</Stack>
{clearable && <ItemDeleteTrigger />}
</Item>
))
}
interface FileUploadListProps extends ItemsBaseProps {}
export const List = forwardRef<HTMLUListElement, FileUploadListProps>(
function FileUploadList(props, ref) {
const { showSize, clearable, files, ...rest } = props
return (
<ItemGroup ref={ref} {...rest}>
<Items showSize={showSize} clearable={clearable} files={files} />
</ItemGroup>
)
},
)
export interface FileTextProps extends HTMLStyledProps<'span'> {
fallback?: string | undefined
}
export const FileText = forwardRef<HTMLSpanElement, FileTextProps>(
function FileUploadFileText(props, ref) {
const { fallback = 'Select file(s)', ...rest } = props
const fileUpload = useFileUploadContext()
const acceptedFiles = fileUpload.acceptedFiles
const fileText = useMemo(() => {
if (acceptedFiles.length === 1) {
return acceptedFiles[0].name
}
if (acceptedFiles.length > 1) {
return `${acceptedFiles.length} files`
}
return fallback
}, [acceptedFiles, fallback])
return (
<Span
ref={ref}
data-placeholder={fileText === fallback ? '' : undefined}
data-scope="file-upload"
data-part="file-text"
{...rest}
>
{fileText}
</Span>
)
},
)
Integrate Recipe
Integrate this recipe in to your Panda config.
import { fileUploadAnatomy } from '@ark-ui/react/file-upload'
import { defineSlotRecipe } from '@pandacss/dev'
export const fileUpload = defineSlotRecipe({
className: 'file-upload',
slots: fileUploadAnatomy.keys(),
base: {
root: {
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'full',
},
label: {
textStyle: 'label',
},
dropzone: {
alignItems: 'center',
background: 'gray.surface.bg',
borderRadius: 'l3',
borderStyle: 'dashed',
borderWidth: '2px',
display: 'flex',
flexDirection: 'column',
focusVisibleRing: 'outside',
justifyContent: 'center',
transition: 'backgrounds',
width: 'full',
_dragging: {
background: 'gray.surface.bg.hover',
borderStyle: 'solid',
borderColor: 'colorPalette.solid.bg',
},
},
item: {
alignItems: 'start',
animationDuration: 'normal',
animationName: 'fade-in',
background: 'gray.surface.bg',
borderRadius: 'l3',
borderWidth: '1px',
display: 'flex',
pos: 'relative',
width: 'full',
},
itemGroup: {
display: 'flex',
alignItems: 'start',
flexDirection: 'column',
width: 'full',
},
itemName: {
color: 'fg.default',
fontWeight: 'medium',
},
itemSizeText: {
color: 'fg.muted',
},
itemDeleteTrigger: {
color: 'fg.subtle',
},
itemPreviewImage: {
aspectRatio: '1',
objectFit: 'cover',
maxW: '20',
borderRadius: 'l2',
},
},
defaultVariants: {
size: 'md',
},
variants: {
size: {
md: {
root: { gap: '4' },
dropzone: { px: '6', py: '4', minHeight: 'xs', gap: '0' },
item: { p: '4', gap: '3', textStyle: 'sm' },
itemGroup: { gap: '3' },
itemDeleteTrigger: { _icon: { boxSize: '4' } },
},
},
},
})
Usage
import { FileUpload } from '@/components/ui'
<FileUpload.Root>
<FileUpload.HiddenInput />
<FileUpload.Label />
<FileUpload.Dropzone>
<FileUpload.DropzoneContent />
</FileUpload.Dropzone>
<FileUpload.Trigger />
<FileUpload.ItemGroup>
<FileUpload.Item>
<FileUpload.ItemPreview />
<FileUpload.ItemFileName />
<FileUpload.ItemSizeText />
<FileUpload.ItemDeleteTrigger />
</FileUpload.Item>
</FileUpload.ItemGroup>
</FileUpload.Root>
Shortcuts
The FileUpload component also provides a set of shortcuts for common use cases.
FileUpload.Items
By default, the FileUpload.Items shortcut renders the list of uploaded files.
<FileUpload.ItemGroup>
<FileUpload.Context>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUpload.Item key={file.name} file={file}>
<FileUpload.ItemPreview />
<FileUpload.ItemName />
<FileUpload.ItemSizeText />
<FileUpload.ItemDeleteTrigger />
</FileUpload.Item>
))
}
</FileUpload.Context>
</FileUpload.ItemGroup>
This might be more concise if you don't need to customize the file upload items:
<FileUpload.ItemGroup>
<FileUpload.Items />
</FileUpload.ItemGroup>
FileUpload.List
The FileUpload.List shortcut renders the list of uploaded files. It composes the FileUpload.ItemGroup and FileUpload.Items components.
<FileUpload.List />
is the same as:
<FileUpload.ItemGroup>
<FileUpload.Items />
</FileUpload.ItemGroup>
Examples
Accepted Files
Specify the accepted file types for upload using the accept prop.
Multiple Files
Upload multiple files at once using the maxFiles prop.
Image Preview
Use the FileUpload.ItemPreviewImage component to render a preview of image files.
Directory
Use the directory prop to allow selecting a directory instead of a file.
Media Capture
Use the capture prop to capture and upload media directly from the device camera or microphone.
The capture prop is only supported on mobile devices.
Dropzone
Drop multiple files inside the dropzone. Use the maxFiles prop to limit the number of files that can be uploaded at once.
Input
Use the FileInput component to create a trigger that looks like a text input.
Props
Root
| Prop | Default | Type |
|---|---|---|
size | 'md' | 'md' |
allowDrop | true | booleanWhether to allow drag and drop in the dropzone element |
locale | \en-US\ | stringThe current locale. Based on the BCP 47 definition. |
maxFiles | 1 | numberThe maximum number of files |
maxFileSize | Infinity | numberThe maximum file size in bytes |
minFileSize | 0 | numberThe minimum file size in bytes |
preventDocumentDrop | true | booleanWhether to prevent the drop event on the document |
accept | Record<string, string[]> | FileMimeType | FileMimeType[]The accept file types | |
acceptedFiles | File[]The controlled accepted files | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
capture | 'user' | 'environment'The default camera to use when capturing media | |
defaultAcceptedFiles | File[]The default accepted files when rendered. Use when you don't need to control the accepted files of the input. | |
directory | booleanWhether to accept directories, only works in webkit browsers | |
disabled | booleanWhether the file input is disabled | |
ids | Partial<{
root: string
dropzone: string
hiddenInput: string
trigger: string
label: string
item: (id: string) => string
itemName: (id: string) => string
itemSizeText: (id: string) => string
itemPreview: (id: string) => string
}>The ids of the elements. Useful for composition. | |
invalid | booleanWhether the file input is invalid | |
name | stringThe name of the underlying file input | |
onFileAccept | (details: FileAcceptDetails) => voidFunction called when the file is accepted | |
onFileChange | (details: FileChangeDetails) => voidFunction called when the value changes, whether accepted or rejected | |
onFileReject | (details: FileRejectDetails) => voidFunction called when the file is rejected | |
required | booleanWhether the file input is required | |
transformFiles | (files: File[]) => Promise<File[]>Function to transform the accepted files to apply transformations | |
translations | IntlTranslationsThe localized messages to use. | |
validate | (file: File, details: FileValidateDetails) => FileError[] | nullFunction to validate a file |
Dropzone
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
disableClick | booleanWhether to disable the click event on the dropzone |
Item
| Prop | Default | Type |
|---|---|---|
file* | File | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. |
ItemPreview
| Prop | Default | Type |
|---|---|---|
type | '.*' | stringThe file type to match against. Matches all file types by default. |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. |