RHFFilePicker
Advanced file picker with drag-and-drop support integrated with React Hook Form
You are reading the v1 docs. v2 renamed several props and replaced GeckoUIPortal with GeckoUIProvider. Go to the v2 docs.
Live examples are only rendered in the v2 docs.
RHFFilePicker
An advanced file picker component with drag-and-drop support, directory selection, and duplicate detection integrated with React Hook Form. Perfect for uploading multiple files or entire directories.
Installation
import { RHFFilePicker } from '@geckoui/geckoui';Basic Usage
import { useForm, FormProvider } from 'react-hook-form';
import { RHFFilePicker } from '@geckoui/geckoui';
function Example() {
const methods = useForm({
defaultValues: {
basicFiles: []
}
});
return (
<FormProvider {...methods}>
<RHFFilePicker name="basicFiles" />
</FormProvider>
);
}Props API
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Field name (required) |
control | Control | Auto-injected | Optional: Pass explicitly for nested forms or custom form context |
rules | RegisterOptions | - | Inline validation rules |
accept | string | * | File types to accept (e.g., "image/*", ".pdf") |
keepOldFiles | boolean | false | Keep previously selected files when adding new ones |
removeDuplicates | boolean | false | Automatically remove duplicate files(Only works if keepOldFiles is true) |
transform | (files) => files | - | Transform files before setting them |
onChange | (files, newFiles) => void | - | Callback when files change |
onError | (error) => void | - | Callback when an error occurs |
render | Function | - | Custom render function with access to picker state |
Examples
With Validation (Zod)
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { RHFFilePicker, RHFInputGroup, Button } from '@geckoui/geckoui';
const schema = z.object({
validationImages: z
.array(z.custom<File>())
.min(1, 'At least one image is required')
.max(10, 'Maximum 10 images allowed')
.refine(
(files) => files.every((file) => file.size <= 5000000),
'Each file must be less than 5MB'
)
.refine(
(files) => files.every((file) => ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)),
'Only JPEG, PNG, and WebP images are allowed'
)
});
function Example() {
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onBlur',
defaultValues: {
validationImages: []
}
});
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(console.log)}>
<RHFInputGroup label="Product Images" required>
<RHFFilePicker name="validationImages" accept="image/*" />
</RHFInputGroup>
<Button type="submit">Upload Images</Button>
</form>
</FormProvider>
);
}Keep Old Files
Accumulate files across multiple selections:
<RHFFilePicker name="accumulatedFiles" keepOldFiles />Remove Duplicates
If you set keepOldFiles, you can also enable removeDuplicates to avoid duplicate files:
So, if a user selects the same file multiple times, only one instance will be kept.
<RHFFilePicker name="uniqueDocuments" keepOldFiles removeDuplicates />Custom Render with Image Grid
<RHFFilePicker
name="customImages"
accept="image/*"
render={({
dropzoneRef,
dragging,
loading,
field: { value: files, onChange },
openFilePicker,
}) => {
const handleRemove = (preview: string) => {
onChange(files.filter((f) => f.preview !== preview));
};
return (
<div className="flex flex-col gap-3">
{/* Compact upload area */}
<div
ref={dropzoneRef}
onClick={() => openFilePicker()}
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed p-4 transition-colors ${
dragging ? 'border-primary-400 bg-primary-50' : 'border-gray-300 hover:border-primary-300'
}`}
>
<div className="flex items-center gap-3 text-sm text-gray-500">
<UploadIcon className="h-5 w-5" />
<span>{dragging ? 'Drop images here' : 'Click to upload or drag and drop'}</span>
</div>
{loading && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-white/50">
<Spinner className="h-5 w-5" />
</div>
)}
</div>
{/* Image grid */}
{!!files?.length && (
<div className="grid grid-cols-4 gap-2 sm:grid-cols-5 md:grid-cols-6">
{files.map((file) => (
<div key={file.preview} className="group relative aspect-square">
<img
src={file.preview}
alt={file.name}
className="h-full w-full rounded-lg border object-cover"
/>
<button
type="button"
onClick={() => handleRemove(file.preview)}
className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-white opacity-0 shadow-sm hover:bg-red-600 group-hover:opacity-100"
>
<XIcon className="h-3 w-3" />
</button>
</div>
))}
</div>
)}
</div>
);
}}
/>With Callbacks
const handleChange = (files: FilePickerFile[], newFiles: FilePickerFile[]) => {
console.log('All files:', files);
console.log('New files:', newFiles);
};
const handleError = (error: Error) => {
console.error('File picker error:', error);
alert(`Error: ${error.message}`);
};
<RHFFilePicker
name="callbackFiles"
onChange={handleChange}
onError={handleError}
/>Complete Form Example
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { RHFFilePicker, RHFInputGroup, Button } from '@geckoui/geckoui';
const schema = z.object({
projectFiles: z
.array(z.custom<File>())
.min(1, 'At least one file is required')
.refine(
(files) => files.every((file) => file.size <= 10000000),
'Each file must be less than 10MB'
),
screenshots: z
.array(z.custom<File>())
.min(3, 'At least 3 screenshots are required')
.max(10, 'Maximum 10 screenshots allowed')
.refine(
(files) => files.every((file) => ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)),
'Only JPEG, PNG, and WebP images are allowed'
)
});
function ProjectForm() {
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onBlur',
defaultValues: {
projectFiles: [],
screenshots: []
}
});
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(console.log)}>
<RHFInputGroup label="Project Files" required>
<RHFFilePicker name="projectFiles" />
</RHFInputGroup>
<RHFInputGroup label="Screenshots" required>
<RHFFilePicker name="screenshots" accept="image/*" />
</RHFInputGroup>
<Button type="submit">Submit Project</Button>
</form>
</FormProvider>
);
}Inline Rules Validation
For simple validation, use the rules prop:
<RHFFilePicker
name="files"
rules={{ required: 'Please upload at least one file' }}
/>For complex forms, we recommend using a schema resolver (Zod, Yup) instead.
File Properties
Each file in the picker has these properties:
interface FilePickerFile extends File {
preview: string;
path: string;
editableName: string;
}preview: Object URL for displaying file contentpath: File system path (from directory picker or webkitRelativePath)editableName: Editable name property (since File.name is read-only)
Drag and Drop
RHFFilePicker automatically handles drag-and-drop:
- Drag files over the dropzone
- The
draggingstate becomestrue - Drop files to add them
- Files are processed and previews are created
Access drag state in custom render:
render={({ dragging, dropzoneRef }) => (
<div
ref={dropzoneRef}
className={dragging ? 'border-blue-400' : 'border-gray-300'}
>
Drop files here
</div>
)}Directory Selection
Select entire directories using the openFilePicker function:
render={({ openFilePicker }) => (
<Button onClick={() => openFilePicker({ directory: true })}>
Select Directory
</Button>
)}This preserves the directory structure in the path property of each file.
Duplicate Detection
When removeDuplicates={true}:
- Files with identical names and sizes are considered duplicates
- Only the first occurrence is kept
- Duplicates are automatically filtered out
Files also have a duplicatedWith property containing an array of files they duplicate.
File Transformation
Transform files before they're set in the form:
const transform = async (files: FilePickerFile[]) => {
return files.map(file => {
file.editableName = file.name.toLowerCase();
return file;
});
};
<RHFFilePicker name="files" transform={transform} />File Types
The accept prop accepts standard file input values:
accept="image/*"
accept=".pdf"
accept=".jpg,.jpeg,.png"
accept="video/*"
accept=".doc,.docx,application/pdf"Validation with Zod
Validate file arrays with Zod:
z.array(z.custom<File>())
.min(1, 'At least one file required')
.max(10, 'Maximum 10 files')
.refine(
(files) => files.every((file) => file.size <= 5000000),
'Each file must be less than 5MB'
)
.refine(
(files) => files.every((file) =>
['image/jpeg', 'image/png'].includes(file.type)
),
'Only JPEG and PNG allowed'
)Error States
RHFFilePicker automatically displays error states:
- Red border via
data-errorattribute on the root element when validation fails - Use with RHFInputGroup for complete error display
<RHFInputGroup label="Upload Files" required>
<RHFFilePicker name="files" />
</RHFInputGroup>Tip: Use mode: 'onBlur' to validate after file selection:
const methods = useForm({
resolver: zodResolver(schema),
mode: 'onBlur'
});Loading States
The picker automatically manages loading states:
render={({ loading }) => (
loading ? <Spinner /> : <div>Ready to upload</div>
)}Loading occurs during:
- File processing
- Preview URL generation
- Transform function execution
Custom Rendering
The render prop provides access to picker state:
render={({
dropzoneRef,
dragging,
loading,
openFilePicker,
files,
field,
fieldState,
formState
}) => (
<div ref={dropzoneRef}>
{/* Custom UI */}
</div>
)}Important: Always attach dropzoneRef to your dropzone element for drag-and-drop to work.
Accessibility
- Keyboard navigable buttons
- Screen reader compatible
- Proper ARIA attributes for loading and error states
- Focus management for file picker dialogs
Performance Tips
- Use
removeDuplicatesto prevent duplicate uploads - Set file size limits in validation
- Limit file count with Zod
.max() - Use
acceptto restrict file types before selection
Related Components
- RHFFileInput - Simple file input for single/multiple files
- RHFInputGroup - Label + input + error wrapper
- RHFError - Error message display