Skip to main content

How to Build a Custom Image Uploader in Salesforce LWC & Apex

In plain words: A custom image uploader in LWC reads a file selected in the browser, encodes it into a Base64 string using JavaScript's FileReader API, and passes that payload to an Apex controller to store it directly in Salesforce Files or send it to an external server.

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

Generate Component via CLI:
# 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());
        }
    }
}
Warning Trap: The legacy 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.
360 Architecture Summary:
  • 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 calling EncodingUtil.base64Decode().
  • Security Enforcement: Always run Apex controllers with sharing and utilize WITH USER_MODE in queries.

Step 4: Deploy and Add to Lightning App Builder

Deployment Steps:
  • Deploy changes to your org: sf project deploy start.
  • Open your target Record Page in Setup > Lightning App Builder.
  • Locate imageUploader in the Custom Components pane and drop it onto the page layout.
  • Save and Activate your Lightning Page.
Core Takeaway: Converting image inputs to Base64 in LWC and decoding them into ContentVersion records in Apex allows custom validation, real-time previewing, and standard Salesforce Files integration.