While standard components like lightning-file-upload work for basic attachments, creating a custom file uploader gives you full control over client-side file compression, validation, custom UI preview states, and tailored backend processing.
Prerequisites
- A Salesforce Developer Edition, Sandbox, or Scratch Org.
- Salesforce CLI (
sf) configured and authenticated to your org. - Basic knowledge of JavaScript FileReader and Apex DML operations.
Step 1: Set Up the LWC Bundle
# Create the component
sf lightning generate component -n imageUploader -d force-app/main/default/lwc --type lwc
Update imageUploader.js-meta.xml to expose the component across Lightning App, Record, and Home pages:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
Step 2: Build the Component Markup & JavaScript
Add a file input element, upload action button, and loading spinner in imageUploader.html:
<template>
<lightning-card title="Image Uploader" icon-name="utility:image">
<div class="slds-p-around_medium">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Uploading..." size="small"></lightning-spinner>
</template>
<lightning-input
type="file"
label="Choose Image"
accept="image/*"
onchange={handleFilesChange}>
</lightning-input>
<template lwc:if={fileName}>
<p class="slds-m-top_small slds-text-color_weak">Selected File: <strong>{fileName}</strong></p>
</template>
<lightning-button
class="slds-m-top_medium"
variant="brand"
label="Upload Image"
onclick={handleUpload}
disabled={isButtonDisabled}>
</lightning-button>
</div>
</lightning-card>
</template>
In imageUploader.js, read the file as a Data URL and extract the pure Base64 content to send to Apex:
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import uploadImage from '@salesforce/apex/UploadImageController.uploadImage';
export default class ImageUploader extends LightningElement {
@api recordId;
fileData;
isLoading = false;
get fileName() {
return this.fileData ? this.fileData.fileName : '';
}
get isButtonDisabled() {
return !this.fileData || this.isLoading;
}
handleFilesChange(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1];
this.fileData = {
fileName: file.name,
base64Data: base64,
recordId: this.recordId
};
};
reader.readAsDataURL(file);
}
handleUpload() {
if (!this.fileData) return;
this.isLoading = true;
uploadImage({
fileName: this.fileData.fileName,
base64Data: this.fileData.base64Data,
recordId: this.fileData.recordId
})
.then(() => {
this.showToast('Success', 'Image uploaded successfully!', 'success');
this.fileData = null;
})
.catch(error => {
this.showToast('Upload Error', error?.body?.message || 'Failed to upload image.', 'error');
})
.finally(() => {
this.isLoading = false;
});
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}
Step 3: Create the Apex Controller
Create UploadImageController.cls. Using modern Salesforce standards, images should be stored as ContentVersion (Salesforce Files) linked via ContentDocumentLink, rather than legacy attachments.
public with sharing class UploadImageController {
@AuraEnabled
public static void uploadImage(String fileName, String base64Data, Id recordId) {
try {
if (String.isBlank(base64Data) || String.isBlank(fileName)) {
throw new AuraHandledException('File data cannot be empty.');
}
// Create ContentVersion (Salesforce File)
ContentVersion cv = new ContentVersion();
cv.Title = fileName;
cv.PathOnClient = '/' + fileName;
cv.VersionData = EncodingUtil.base64Decode(base64Data);
cv.IsMajorVersion = true;
insert cv;
// Link file to record if recordId is present
if (recordId != null) {
Id contentDocumentId = [
SELECT ContentDocumentId
FROM ContentVersion
WHERE Id = :cv.Id
WITH USER_MODE
LIMIT 1
].ContentDocumentId;
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.ContentDocumentId = contentDocumentId;
cdl.LinkedEntityId = recordId;
cdl.ShareType = 'V';
cdl.Visibility = 'AllUsers';
insert cdl;
}
} catch (Exception ex) {
throw new AuraHandledException(ex.getMessage());
}
}
}
Attachment object is deprecated for new implementations. Always use ContentVersion and ContentDocumentLink to ensure your uploaded images are accessible within the Salesforce Files architecture and Lightning Experience components.
- Heap Size Limits: Keep client-side Base64 payloads under ~4 MB to prevent hitting the synchronous Apex 6 MB heap size limit.
- Data Encoding:
FileReader.readAsDataURL()includes metadata prefixes (e.g.,data:image/png;base64,) that must be split before callingEncodingUtil.base64Decode(). - Security Enforcement: Always run Apex controllers
with sharingand utilizeWITH USER_MODEin queries.
Step 4: Deploy and Add to Lightning App Builder
- Deploy changes to your org:
sf project deploy start. - Open your target Record Page in Setup > Lightning App Builder.
- Locate
imageUploaderin the Custom Components pane and drop it onto the page layout. - Save and Activate your Lightning Page.