AI Dropzone
A drag-and-drop component that provides an intuitive interface for users to upload files by dragging them into a designated area.
Accessibility note: The dropzone is only accessible via a drag and drop interaction with the mouse. Dropzone should always be used in combination with other file upload methods that are accessible to keyboard users, such as a button.
Loading...
Overview
Resources
Install
yarn add @camp/ai-dropzoneUsage
Basic dropzone
The simplest implementation with just the required props.
import { AiDropzone } from '@camp/ai-dropzone';
function BasicDropzone() {
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
// Handle file validation and processing here
};
return (
<div style={{ height: '256px', width: '600px', position: 'relative', border: '1px solid red' }}>
<AiDropzone
title="Drop files here"
validateFiles={handleFileValidation}
>
<div style={{ border: '1px solid black' }}>
Drag files over to see AI Dropzone. Dropzone will overlay on top of the child
element(s) and will match the size of its parent wrapping container. In this story,
the wrapping container is shown with a red border, and the child element is shown with
a black border.
</div>
</AiDropzone>
</div>
);
}Dropzone with file validation
A dropzone that includes validation logic and error messaging for file types, sizes, and multiple files. Message translation is handled by the useTranslation hook and some translations are already available for use for these error messages.
import { useTranslation } from '@activecampaign/core-translations-client';
import { useState, useCallback } from 'react';
import { AiDropzone } from '@camp/ai-dropzone';
import { AiText } from '@camp/ai-text';
import { AiButton } from '@camp/ai-button';
function ValidatedDropzone() {
const [files, setFiles] = useState<File[]>([]);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
const isFileTypeAccepted = (file: File, acceptedTypes: string[]): boolean => {
if (!acceptedTypes || acceptedTypes.length === 0) return true;
return acceptedTypes.some((type) => {
if (type.startsWith('.')) {
// Handle file extensions like '.pdf', '.jpg'
return file.name.toLowerCase().endsWith(type.toLowerCase());
} else if (type.includes('*')) {
// Handle wildcard MIME types like 'image/*', 'text/*'
const [mainType] = type.split('/');
return file.type.startsWith(mainType);
} else {
// Handle exact MIME types like 'text/plain', 'image/jpeg'
return file.type === type;
}
});
};
// Helper function to format file size for error messages
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const validateFiles = useCallback(
(files: File[]) => {
const acceptedFileTypes = ['image/*', '.pdf', '.csv'];
const maxFileSize = 10 * 1024 * 1024; // 10MB
// If multiple files are not allowed, return an error
if (files.length > 1) {
setError(t('camp:dropzone-multiple-files-not-allowed'));
return;
}
// Check if file types are accepted
if (acceptedFileTypes && acceptedFileTypes.length > 0) {
const invalidFiles = files.filter((file) => !isFileTypeAccepted(file, acceptedFileTypes));
if (invalidFiles.length > 0) {
const invalidFileNames = invalidFiles.map((f) => f.name).join(', ');
const acceptedTypesText = acceptedFileTypes.join(', ');
setError(
t('camp:dropzone-invalid-file-types', { invalidFileNames, acceptedTypesText }),
);
return;
}
}
// If file size is too large, return an error
if (maxFileSize && maxFileSize > 0) {
const oversizedFiles = files.filter((file) => file.size > maxFileSize);
if (oversizedFiles.length > 0) {
const oversizedFileNames = oversizedFiles
.map((f) => `${f.name} (${formatFileSize(f.size)})`)
.join(', ');
const maxSizeText = formatFileSize(maxFileSize);
setError(t('camp:dropzone-files-too-large', { oversizedFileNames, maxSizeText }));
return;
}
}
// If all files are valid, add them to the files state
setFiles(files);
},
[t],
);
return (
<div
style={{ height: '256px', width: '600px', position: 'relative', border: '1px solid red' }}
>
<AiDropzone title="Drop files here" validateFiles={validateFiles}>
<div style={{ border: '1px solid black', padding: '8px' }}>
<AiText appearance="body-small" color="default">
This story demonstrates how to use the AiDropzone component with validation. Drag
files over to see AI Dropzone. Dropzone will overlay on top of the child element(s)
and will match the size of its parent wrapping container. In this story, the wrapping
container is shown with a red border, and the child element is shown with a black
border.
</AiText>
</div>
<div style={{ padding: '8px' }}>
{files.length === 0 && (
<AiText appearance="body-small" color="default">
No files added
</AiText>
)}
{files.length > 0 && (
<>
<AiText appearance="body-small" color="default">
No files added
</AiText>
{files.map((file) => (
<AiText appearance="body-small" color="default" key={file.name}>
{file.name}
</AiText>
))}
<AiButton
style={{ marginLeft: '8px', marginTop: '8px' }}
onClick={() => setFiles([])}
size="small"
appearance="secondary"
>
Clear files
</AiButton>
</>
)}
</div>
<div style={{ padding: '8px', color: 'red' }}>
{error && (
<>
<AiText appearance="body-small" color="default">
Error: {error}
</AiText>
<div style={{ marginLeft: '8px', marginTop: '8px' }}>
<AiButton onClick={() => setError(null)} size="small" appearance="secondary">
Clear error
</AiButton>
</div>
</>
)}
</div>
</AiDropzone>
</div>
);
}Available translations
These error messages are available for use from @activecampaign/core-translations-client.
"camp:dropzone-multiple-files-not-allowed": {
"translation": "Multiple files not allowed. Please drop only one file at a time.",
"notes": "Dropzone error when multiple files are dropped"
},
"camp:dropzone-invalid-file-types": {
"translation": "Invalid file type(s): {invalidFileNames}. Accepted types: {acceptedTypesText}",
"notes": "Dropzone error listing invalid files and accepted types. Placeholders: invalidFileNames, acceptedTypesText"
},
"camp:dropzone-files-too-large": {
"translation": "File(s) too large: {oversizedFileNames}. Maximum allowed size: {maxSizeText}",
"notes": "Dropzone error listing files over size and max allowed size. Placeholders: oversizedFileNames, maxSizeText"
},Always visible
By default, the dropzone is only visible when the user is dragging a file over it. The dropzone can be set to always be visible by setting the alwaysVisible prop to true. This can be useful for testing the visual appearance of the dropzone in development, or for accessibility purposes in certain cases.
import { AiDropzone } from '@camp/ai-dropzone';
function AlwaysVisibleDropzone() {
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
// Handle file validation and processing here
};
return (
<div style={{ height: '256px', width: '600px', position: 'relative' }}>
<AiDropzone title="Drop files here" alwaysVisible validateFiles={handleFileValidation}>
{/* Your child elements here */}
</AiDropzone>
</div>
);
}Allow multiple files
By default, the dropzone input is not configured for multiple files. The dropzone can be set to allow multiple files by setting the allowMultiple prop to true. This updates the hidden input element to include the multiple attribute.
Please note: This does not include any validation logic or error messaging. You will need to implement your own validation logic to handle multiple files for your use case.
import { AiDropzone } from '@camp/ai-dropzone';
function AllowMultipleFilesDropzone() {
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
// Handle file validation and processing here
};
return (
<div style={{ height: '256px', width: '600px', position: 'relative' }}>
<AiDropzone title="Drop files here" alwaysVisible allowMultiple validateFiles={handleFileValidation}>
{/* Your child elements here */}
</AiDropzone>
</div>
);
}Accepted file types
By default, the dropzone input is not configured for any file types. The dropzone can be set to accept specific file types by setting the acceptedFileTypes prop to a comma-separated string of file types. This updates the hidden input element to include the accept attribute with your specified file types. In this example, the hidden input element has an accept attribute of .pdf, .jpg, .png.
Please note: This does not include any validation logic or error messaging. You will need to implement your own validation logic to handle file types for your use case.
import { AiDropzone } from '@camp/ai-dropzone';
function AcceptedFileTypesDropzone() {
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
// Handle file validation and processing here
};
return (
<div style={{ height: '256px', width: '600px', position: 'relative' }}>
<AiDropzone title="Drop files here" alwaysVisible acceptedFileTypes=".pdf, .jpg, .png" validateFiles={handleFileValidation}>
{/* Your child elements here */}
</AiDropzone>
</div>
);
}Drag event callbacks
The dropzone provides optional callbacks for tracking drag interactions, which can be useful for updating UI state, triggering animations, or logging user behavior.
import { AiDropzone } from '@camp/ai-dropzone';
import { useState } from 'react';
function DragTrackingDropzone() {
const [isDragging, setIsDragging] = useState(false);
const [dragCount, setDragCount] = useState(0);
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
setIsDragging(false);
};
const handleDragEnter = () => {
console.log('File drag entered dropzone');
setIsDragging(true);
setDragCount((prev) => prev + 1);
};
const handleDragLeave = () => {
console.log('File drag left dropzone without dropping');
setIsDragging(false);
};
return (
<div style={{ height: '256px', width: '600px', position: 'relative' }}>
<AiDropzone
title="Drop files here"
validateFiles={handleFileValidation}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
<div style={{ border: '1px solid black', padding: '8px' }}>
<p>Drag state: {isDragging ? 'Dragging over dropzone' : 'Not dragging'}</p>
<p>Total drag enter events: {dragCount}</p>
</div>
</AiDropzone>
</div>
);
}Use cases for drag callbacks:
- onDragEnter: Expand input area, show helper text, trigger animations, update state for other components
- onDragLeave: Collapse input area, hide helper text, reset temporary UI changes
Maximum width
Control the maximum width of the dropzone wrapper with the maxWidth prop. This is useful when you want to constrain the dropzone within a specific layout or maintain consistent sizing across different screen sizes.
import { AiDropzone } from '@camp/ai-dropzone';
function ConstrainedWidthDropzone() {
const handleFileValidation = (files: File[]) => {
console.log('Files received:', files);
};
return (
<div style={{ height: '256px', position: 'relative' }}>
<AiDropzone
title="Drop files here"
validateFiles={handleFileValidation}
maxWidth="400px" // Constrain to 400px width
alwaysVisible
>
<div style={{ border: '1px solid black', padding: '8px' }}>
This dropzone has a maximum width of 400px
</div>
</AiDropzone>
</div>
);
}Props reference
The AI Dropzone component supports the following props:
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | Required | Text displayed in the dropzone overlay (e.g., “Drop files here”) |
validateFiles | (files: File[]) => void | Required | Callback function that receives dropped files for validation and processing |
children | ReactNode | — | Content that the dropzone overlays on top of |
alwaysVisible | boolean | false | Keep dropzone visible at all times (useful for testing or specific UX needs) |
allowMultiple | boolean | false | Allow multiple files to be dropped at once (updates hidden input’s multiple attribute) |
acceptedFileTypes | string | — | Comma-separated list of accepted file types (e.g., “.pdf, .jpg, .png” or “image/*”) - updates hidden input’s accept attribute |
maxWidth | string | — | Maximum width of the dropzone wrapper (e.g., “400px”, “50%“) |
onDragEnter | () => void | — | Optional callback fired when a drag enters the dropzone area |
onDragLeave | () => void | — | Optional callback fired when a drag leaves the dropzone without dropping |
Important notes:
- The
acceptedFileTypesandallowMultipleprops only update the hidden input element - they do not provide validation logic - You must implement your own validation logic in the
validateFilescallback - Use the provided translation keys for consistent error messaging
- The dropzone matches the size of its parent container, so ensure the parent has defined dimensions
Best practices
✅ DO
- Use clear, action-oriented text in the
titleprop like “Drop files here” or “Upload images” - Specify accepted file types using
acceptedFileTypesprop - Always implement file validation in the
validateFilescallback - Provide helpful error messages for invalid files in your validation logic
- Wrap the component in a container with defined dimensions for proper overlay behavior
- Provide other avenues for file upload that are accessible to keyboard users, such as a button
- Use
onDragEnterandonDragLeavecallbacks to provide visual feedback during drag interactions - Set
maxWidthwhen you need to constrain the dropzone within specific layouts - Use
allowMultiplewhen accepting batch file uploads
🚫 DON’T
- Don’t use vague text like “Upload” or “File dropzone” in the
titleprop - Don’t rely only on
acceptedFileTypesorallowMultiplefor validation - implement proper validation logic - Don’t use technical jargon or untranslated text in title or error messages
- Don’t forget to handle both
onDragEnterandonDragLeaveif you’re updating UI state based on drag events
Accessibility
Keyboard support
- The dropzone is only accessible via a drag and drop interaction with the mouse. Dropzone should always be used in combination with other file upload methods that are accessible to keyboard users, such as a button.