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-dropzone
Usage
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.
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"
},
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>
);
}
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>
);
}
Content guidelines
✅ DO
- Use clear, action-oriented text in the
title
prop like “Drop files here” or “Upload images” - Specify accepted file types using
acceptedFileTypes
prop - Always implement file validation in the
validateFiles
callback - 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
🚫 DON’T
- Don’t use vague text like “Upload” or “File dropzone” in the
title
prop - Don’t rely only on
acceptedFileTypes
orallowMultiple
for validation - implement proper validation logic - Don’t use technical jargon or untranslated text in title or error messages
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.