Skip to main content

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
File | Attachment
required
File or attachment object
onRemove
() => void
Callback when remove button is clicked
onDownload
() => void
Callback when download button is clicked

Examples

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>
  );
};

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/

Build docs developers (and LLMs) love