Mail-Specific Components
Proton provides specialized components designed specifically for mail applications, including address inputs, autocomplete, and attachment handling.AddressesAutocomplete
Autocomplete component for email addresses. Location:components/addressesAutocomplete/
Usage
import AddressesAutocomplete from '@proton/components/components/addressesAutocomplete';
const MyEmailComposer = () => {
const [recipients, setRecipients] = useState([]);
return (
<AddressesAutocomplete
value={recipients}
onChange={setRecipients}
placeholder="To: "
/>
);
};
Features
- Email address validation
- Contact suggestions
- Multiple recipient support
- Keyboard navigation
- Copy/paste support
- Contact group expansion
AddressesInput
Input component for entering email addresses. Location:components/addressesInput/
Usage
import AddressesInput from '@proton/components/components/addressesInput';
const RecipientInput = () => {
const [addresses, setAddresses] = useState([]);
return (
<AddressesInput
value={addresses}
onChange={setAddresses}
placeholder="Enter email addresses"
/>
);
};
AttachedFile
Component for displaying attached files. Location:components/attachedFile/
Usage
import AttachedFile from '@proton/components/components/attachedFile';
const AttachmentList = ({ files }) => {
return (
<div className="flex flex-column gap-2">
{files.map(file => (
<AttachedFile
key={file.id}
file={file}
onRemove={() => handleRemove(file.id)}
/>
))}
</div>
);
};
Props
File or attachment object
Callback when remove button is clicked
Callback when download button is clicked
Examples
- Email Composer
- Contact Autocomplete
- Attachment Upload
const EmailComposer = () => {
const [to, setTo] = useState([]);
const [cc, setCc] = useState([]);
const [bcc, setBcc] = useState([]);
const [subject, setSubject] = useState('');
const [body, setBody] = useState('');
const [attachments, setAttachments] = useState([]);
const handleAttachment = (files: File[]) => {
setAttachments([...attachments, ...files]);
};
const handleRemoveAttachment = (index: number) => {
setAttachments(attachments.filter((_, i) => i !== index));
};
return (
<div className="flex flex-column gap-4">
<AddressesAutocomplete
label="To:"
value={to}
onChange={setTo}
/>
<AddressesAutocomplete
label="Cc:"
value={cc}
onChange={setCc}
/>
<AddressesAutocomplete
label="Bcc:"
value={bcc}
onChange={setBcc}
/>
<Input
placeholder="Subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
/>
<Editor
value={body}
onChange={setBody}
onAddAttachments={handleAttachment}
/>
{attachments.length > 0 && (
<div className="flex flex-column gap-2">
<h4>Attachments</h4>
{attachments.map((file, index) => (
<AttachedFile
key={index}
file={file}
onRemove={() => handleRemoveAttachment(index)}
/>
))}
</div>
)}
<div className="flex gap-2">
<Button color="norm">Send</Button>
<Button>Save Draft</Button>
</div>
</div>
);
};
const ContactField = () => {
const [selected, setSelected] = useState([]);
const contacts = useContacts();
const getSuggestions = (query: string) => {
return contacts.filter(contact =>
contact.email.toLowerCase().includes(query.toLowerCase()) ||
contact.name.toLowerCase().includes(query.toLowerCase())
);
};
return (
<AddressesAutocomplete
value={selected}
onChange={setSelected}
getSuggestions={getSuggestions}
placeholder="Add recipients..."
multiple
/>
);
};
const AttachmentUploader = () => {
const [files, setFiles] = useState<File[]>([]);
const [uploading, setUploading] = useState(false);
const handleFileSelect = (newFiles: File[]) => {
setFiles([...files, ...newFiles]);
};
const handleUpload = async () => {
setUploading(true);
try {
await uploadFiles(files);
} finally {
setUploading(false);
}
};
return (
<div>
<FileInput
onChange={(e) => handleFileSelect(Array.from(e.target.files || []))}
multiple
>
Attach files
</FileInput>
{files.length > 0 && (
<div className="mt-4">
{files.map((file, index) => (
<AttachedFile
key={index}
file={file}
onRemove={() => {
setFiles(files.filter((_, i) => i !== index));
}}
/>
))}
</div>
)}
{files.length > 0 && (
<Button
onClick={handleUpload}
loading={uploading}
className="mt-4"
>
Upload {files.length} file{files.length > 1 ? 's' : ''}
</Button>
)}
</div>
);
};
Best Practices
Email Validation
const validateEmail = (email: string): boolean => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const EmailInput = () => {
const [addresses, setAddresses] = useState([]);
const [error, setError] = useState('');
const handleChange = (newAddresses) => {
const invalid = newAddresses.find(addr => !validateEmail(addr.email));
if (invalid) {
setError(`Invalid email: ${invalid.email}`);
} else {
setError('');
setAddresses(newAddresses);
}
};
return (
<div>
<AddressesAutocomplete
value={addresses}
onChange={handleChange}
/>
{error && <span className="text-sm color-danger">{error}</span>}
</div>
);
};
Contact Groups
const ContactGroupField = () => {
const [recipients, setRecipients] = useState([]);
const groups = useContactGroups();
const expandGroup = (group) => {
return group.members.map(member => ({
email: member.email,
name: member.name,
}));
};
const handleAddRecipient = (recipient) => {
if (recipient.isGroup) {
const members = expandGroup(recipient);
setRecipients([...recipients, ...members]);
} else {
setRecipients([...recipients, recipient]);
}
};
return (
<AddressesAutocomplete
value={recipients}
onChange={setRecipients}
onAddRecipient={handleAddRecipient}
groups={groups}
/>
);
};
File Size Limits
const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25MB
const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100MB
const AttachmentHandler = () => {
const [files, setFiles] = useState<File[]>([]);
const [error, setError] = useState('');
const validateFiles = (newFiles: File[]) => {
const oversized = newFiles.find(f => f.size > MAX_FILE_SIZE);
if (oversized) {
setError(`File too large: ${oversized.name} (max 25MB)`);
return false;
}
const totalSize = [...files, ...newFiles]
.reduce((sum, f) => sum + f.size, 0);
if (totalSize > MAX_TOTAL_SIZE) {
setError('Total attachment size exceeds 100MB');
return false;
}
setError('');
return true;
};
const handleAddFiles = (newFiles: File[]) => {
if (validateFiles(newFiles)) {
setFiles([...files, ...newFiles]);
}
};
return (
<div>
<FileInput onChange={(e) => handleAddFiles(Array.from(e.target.files || []))}>
Attach files
</FileInput>
{error && <p className="text-sm color-danger">{error}</p>}
{/* Attachment list */}
</div>
);
};
Keyboard Shortcuts
const ComposerWithShortcuts = () => {
const editorRef = useRef<EditorActions>(null);
useHotkeys(
document,
[
['Mod+Enter', () => handleSend()],
['Mod+Shift+A', () => handleAttach()],
['Escape', () => handleClose()],
],
{ enableOnFormTags: ['INPUT', 'TEXTAREA'] }
);
return (
<div>
{/* Composer UI */}
</div>
);
};
Accessibility
<AddressesAutocomplete
value={recipients}
onChange={setRecipients}
aria-label="Email recipients"
aria-describedby="recipient-help"
/>
<span id="recipient-help" className="sr-only">
Enter email addresses or select from contacts
</span>
Source Code
View source:- AddressesAutocomplete:
packages/components/components/addressesAutocomplete/ - AddressesInput:
packages/components/components/addressesInput/ - AttachedFile:
packages/components/components/attachedFile/