Salesforce records frequently accumulate documents such as contracts, spec sheets, invoices, and images. While the standard "Files" related list provides basic table access, business users often need a responsive card view with direct preview and download actions. Building this in Lightning Web Components (LWC) with modern Salesforce Files architecture delivers a clean, responsive document gallery directly on the record page.
1. Modern Architecture: Salesforce Files vs. Legacy Attachments
When developing file utilities in Salesforce, it is vital to target the modern file storage architecture rather than legacy objects:
- Legacy Attachments (
Attachment): Deprecated storage model tied directly to a single parent record. Lacks versioning, sharing settings, and modern preview handlers. - Salesforce Files (
ContentDocumentLink&ContentVersion): The standard enterprise storage model. Files are independent entities linked to records via junction objects, enabling multi-record sharing, version tracking, and secure platform previews.
- Query Junction:
ContentDocumentLinkfiltered byLinkedEntityId = :recordId. - Data Payload:
ContentDocument.LatestPublishedVersion(Title, FileType, ContentSize, Extension). - Security Enforcement:
WITH USER_MODEand cacheable wire methods. - Preview Mechanism: Standard Salesforce file preview via
NavigationMixin.
2. Implementing the Apex File Controller
The backend controller queries all files linked to the current record ID, computes readable file sizes, and returns structured data to the front end.
RecordFilesController.cls)
public with sharing class RecordFilesController {
public class FileCardWrapper {
@AuraEnabled public Id documentId;
@AuraEnabled public Id versionId;
@AuraEnabled public String title;
@AuraEnabled public String fileType;
@AuraEnabled public String extension;
@AuraEnabled public String formattedSize;
@AuraEnabled public String downloadUrl;
}
@AuraEnabled(cacheable=true)
public static List<FileCardWrapper> getRelatedFiles(Id recordId) {
List<FileCardWrapper> results = new List<FileCardWrapper>();
if (recordId == null) {
return results;
}
// Query active Salesforce Files linked to this record
List<ContentDocumentLink> links = [
SELECT ContentDocumentId,
ContentDocument.Title,
ContentDocument.FileType,
ContentDocument.FileExtension,
ContentDocument.LatestPublishedVersionId,
ContentDocument.ContentSize
FROM ContentDocumentLink
WHERE LinkedEntityId = :recordId
WITH USER_MODE
ORDER BY ContentDocument.CreatedDate DESC
LIMIT 100
];
for (ContentDocumentLink link : links) {
FileCardWrapper item = new FileCardWrapper();
item.documentId = link.ContentDocumentId;
item.versionId = link.ContentDocument.LatestPublishedVersionId;
item.title = link.ContentDocument.Title;
item.fileType = link.ContentDocument.FileType;
item.extension = link.ContentDocument.FileExtension;
item.downloadUrl = '/sfc/servlet.shepherd/version/download/' + link.ContentDocument.LatestPublishedVersionId;
// Format size to KB / MB
Long bytes = link.ContentDocument.ContentSize;
if (bytes != null) {
if (bytes < 1024) {
item.formattedSize = bytes + ' B';
} else if (bytes < 1048576) {
item.formattedSize = (bytes / 1024) + ' KB';
} else {
item.formattedSize = ((Double) bytes / 1048576).setScale(2) + ' MB';
}
}
results.add(item);
}
return results;
}
}
3. Building the Lightning Web Component
The LWC component renders a responsive card grid using SLDS utilities and enables in-app file previews using the platform navigation service.
viewAllAttachments.html)
<template>
<lightning-card title="Related Files & Attachments" icon-name="standard:file">
<div class="slds-p-around_medium">
<!-- Loading State -->
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading files..." size="small"></lightning-spinner>
</template>
<!-- Files Grid -->
<template lwc:if={hasFiles}>
<div class="slds-grid slds-wrap slds-gutters">
<template for:each={files} for:item="file">
<div key={file.documentId} class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-large-size_1-of-3 slds-m-bottom_small">
<div class="slds-box slds-box_x-small slds-theme_shade slds-radius_medium">
<div class="slds-media slds-media_center">
<div class="slds-media__figure">
<lightning-icon icon-name="doctype:attachment" size="small" alternative-text="Document"></lightning-icon>
</div>
<div class="slds-media__body slds-truncate">
<a href="javascript:void(0);"
class="slds-text-heading_small slds-truncate"
title={file.title}
data-id={file.documentId}
onclick={handleFilePreview}>
{file.title}
</a>
<p class="slds-text-body_small slds-text-color_weak">
{file.extension} • {file.formattedSize}
</p>
</div>
<div class="slds-media__figure slds-media__figure_reverse">
<a href={file.downloadUrl} target="_blank" download class="slds-button slds-button_icon slds-button_icon-border-filled" title="Download">
<lightning-icon icon-name="utility:download" size="xx-small" alternative-text="Download"></lightning-icon>
</a>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<!-- Empty State -->
<template lwc:elseif={noFilesFound}>
<div class="slds-illustration slds-illustration_small slds-text-align_center slds-p-vertical_medium">
<p class="slds-text-body_regular slds-text-color_weak">No files or attachments linked to this record.</p>
</div>
</template>
</div>
</lightning-card>
</template>
viewAllAttachments.js)
import { LightningElement, api, wire, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import getRelatedFiles from '@salesforce/apex/RecordFilesController.getRelatedFiles';
export default class ViewAllAttachments extends NavigationMixin(LightningElement) {
@api recordId;
@track files = [];
isLoading = true;
@wire(getRelatedFiles, { recordId: '$recordId' })
wiredFiles({ error, data }) {
this.isLoading = false;
if (data) {
this.files = data;
} else if (error) {
console.error('Error fetching record files:', error);
this.files = [];
}
}
get hasFiles() {
return this.files && this.files.length > 0;
}
get noFilesFound() {
return !this.isLoading && (!this.files || this.files.length === 0);
}
handleFilePreview(event) {
const docId = event.currentTarget.dataset.id;
// Open standard Salesforce File Preview Modal
this[NavigationMixin.Navigate]({
type: 'standard__namedPage',
attributes: {
pageName: 'filePreview'
},
state: {
selectedRecordId: docId
}
});
}
}
viewAllAttachments.js-meta.xml)
<?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>lightningCommunity__Page</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Account</object>
<object>Contact</object>
<object>Opportunity</object>
<object>Case</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
4. Common Traps & Development Best Practices
Attachment Object in Modern OrgsWriting SOQL against the legacy
Attachment object (SELECT Id FROM Attachment WHERE ParentId = :recordId) ignores files uploaded via Lightning Experience, Salesforce Mobile, and Chatter, which are stored as ContentDocument records. Always query ContentDocumentLink to capture all modern attachments.
- Leverage Cacheable Wire: Marking the Apex method
@AuraEnabled(cacheable=true)ensures rapid client-side rendering and automatic caching across the Lightning Experience session. - Handle Direct Downloads: Construct download URLs using the standard platform servlet path (
/sfc/servlet.shepherd/version/download/{versionId}) to enable one-click local downloads. - Responsive SLDS Grid: Use SLDS responsive sizing classes (
slds-size_1-of-1 slds-medium-size_1-of-2 slds-large-size_1-of-3) so file cards adapt cleanly to mobile, narrow sidebars, and wide main panels.
Summary
Building a custom file and attachment gallery in Lightning Web Components provides a modern, user-friendly document viewing experience on Salesforce record pages. By querying ContentDocumentLink in Apex, formatting file metadata defensively, and wiring up native file previews via NavigationMixin, developers can deliver a fast, responsive document hub that aligns with modern Salesforce architecture.