Skip to Content

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

Loading...

Loading...

Loading...

Loading...

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.

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:

PropTypeDefaultDescription
titlestringRequiredText displayed in the dropzone overlay (e.g., “Drop files here”)
validateFiles(files: File[]) => voidRequiredCallback function that receives dropped files for validation and processing
childrenReactNodeContent that the dropzone overlays on top of
alwaysVisiblebooleanfalseKeep dropzone visible at all times (useful for testing or specific UX needs)
allowMultiplebooleanfalseAllow multiple files to be dropped at once (updates hidden input’s multiple attribute)
acceptedFileTypesstringComma-separated list of accepted file types (e.g., “.pdf, .jpg, .png” or “image/*”) - updates hidden input’s accept attribute
maxWidthstringMaximum width of the dropzone wrapper (e.g., “400px”, “50%“)
onDragEnter() => voidOptional callback fired when a drag enters the dropzone area
onDragLeave() => voidOptional callback fired when a drag leaves the dropzone without dropping

Important notes:

  • The acceptedFileTypes and allowMultiple props only update the hidden input element - they do not provide validation logic
  • You must implement your own validation logic in the validateFiles callback
  • 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 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
  • Use onDragEnter and onDragLeave callbacks to provide visual feedback during drag interactions
  • Set maxWidth when you need to constrain the dropzone within specific layouts
  • Use allowMultiple when accepting batch file uploads

🚫 DON’T

  • Don’t use vague text like “Upload” or “File dropzone” in the title prop
  • Don’t rely only on acceptedFileTypes or allowMultiple for validation - implement proper validation logic
  • Don’t use technical jargon or untranslated text in title or error messages
  • Don’t forget to handle both onDragEnter and onDragLeave if 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.
Last updated on