Layout Patterns
Card Grid (Dashboard)
Responsive card grid that adapts to container width using CSS-only (no JS resize observers).
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--slds-g-spacing-4, 1rem);
padding: var(--slds-g-spacing-4, 1rem);
}
Split View (Master-Detail)
.split-view {
display: grid;
grid-template-columns: 320px 1fr;
gap: var(--slds-g-spacing-4, 1rem);
height: 100%;
}
.split-view__list {
overflow-y: auto;
border-right: var(--slds-g-sizing-border-1) solid var(--slds-g-color-border-1, #e5e5e5);
}
.split-view__detail {
overflow-y: auto;
padding: var(--slds-g-spacing-4, 1rem);
}
Stacked Form
.form-stack {
display: flex;
flex-direction: column;
gap: var(--slds-g-spacing-4, 1rem);
max-width: 600px;
}
.form-stack__section {
padding: var(--slds-g-spacing-4, 1rem);
background: var(--slds-g-color-surface-1, #ffffff);
border-radius: var(--slds-g-radius-border-2, 0.25rem);
border: var(--slds-g-sizing-border-1) solid var(--slds-g-color-border-1, #e5e5e5);
}
.form-stack__actions {
display: flex;
justify-content: flex-end;
gap: var(--slds-g-spacing-3, 0.75rem);
padding-top: var(--slds-g-spacing-4, 1rem);
border-top: var(--slds-g-sizing-border-1) solid var(--slds-g-color-border-1, #e5e5e5);
}
Interaction Patterns
Loading States
Always show a loading indicator. Never leave the user staring at a blank screen.
<class="code-tag">template>
class="code-comment"><!-- Skeleton: structural placeholder while data loads -->
<class="code-tag">template if:true={isLoading}>
<class="code-tag">div class="skeleton-card">
<class="code-tag">div class="skeleton-line skeleton-line--title"></class="code-tag">div>
<class="code-tag">div class="skeleton-line skeleton-line--body"></class="code-tag">div>
<class="code-tag">div class="skeleton-line skeleton-line--body skeleton-line--short"></class="code-tag">div>
</class="code-tag">div>
</class="code-tag">template>
class="code-comment"><!-- Content: rendered after data arrives -->
<class="code-tag">template if:false={isLoading}>
<class="code-tag">div class="content-card">
<class="code-tag">h2>{title}</class="code-tag">h2>
<class="code-tag">p>{description}</class="code-tag">p>
</class="code-tag">div>
</class="code-tag">template>
</class="code-tag">template>
.skeleton-line {
height: var(--slds-g-spacing-3, 0.75rem);
background: var(--slds-g-color-surface-container-2, #f3f3f3);
border-radius: var(--slds-g-radius-border-1, 0.125rem);
margin-bottom: var(--slds-g-spacing-2, 0.5rem);
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-line--title {
width: 40%;
height: var(--slds-g-spacing-4, 1rem);
}
.skeleton-line--short { width: 60%; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
Empty States
Provide guidance, not just "No records found."
<class="code-tag">template>
<class="code-tag">template if:true={isEmpty}>
<class="code-tag">div class="empty-state" role="status">
<class="code-tag">lightning-icon icon-name="utility:info" size="large"></class="code-tag">lightning-icon>
<class="code-tag">h3 class="empty-state__title">{emptyTitle}</class="code-tag">h3>
<class="code-tag">p class="empty-state__message">{emptyMessage}</class="code-tag">p>
<class="code-tag">lightning-button
if:true={showAction}
label={emptyActionLabel}
variant="brand"
onclick={handleEmptyAction}>
</class="code-tag">lightning-button>
</class="code-tag">div>
</class="code-tag">template>
</class="code-tag">template>
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--slds-g-spacing-3, 0.75rem);
padding: var(--slds-g-spacing-8, 2rem) var(--slds-g-spacing-4, 1rem);
text-align: center;
color: var(--slds-g-color-on-surface-2, #444444);
}
.empty-state__title {
font-size: var(--slds-g-font-size-5, 1rem);
font-weight: var(--slds-g-font-weight-6, 600);
color: var(--slds-g-color-on-surface-1, #181818);
}
Error Boundaries
Catch and display errors gracefully. Never show raw error text to users.
<class="code-tag">template>
<class="code-tag">template if:true={hasError}>
<class="code-tag">div class="error-boundary" role="alert">
<class="code-tag">lightning-icon icon-name="utility:error" variant="error" size="small"></class="code-tag">lightning-icon>
<class="code-tag">div class="error-boundary__content">
<class="code-tag">p class="error-boundary__title">Something went wrong</class="code-tag">p>
<class="code-tag">p class="error-boundary__detail">{userFriendlyError}</class="code-tag">p>
<class="code-tag">lightning-button label="Try Again" onclick={handleRetry} variant="neutral">
</class="code-tag">lightning-button>
</class="code-tag">div>
</class="code-tag">div>
</class="code-tag">template>
</class="code-tag">template>
.error-boundary {
display: flex;
gap: var(--slds-g-spacing-3, 0.75rem);
padding: var(--slds-g-spacing-4, 1rem);
background: var(--slds-g-color-error-container-1, #fef1f1);
border: var(--slds-g-sizing-border-1) solid var(--slds-g-color-border-error-1, #ea001e);
border-radius: var(--slds-g-radius-border-2, 0.25rem);
}
Toast / Inline Notifications
Use lightning/platformShowToastEvent for transient feedback. Use inline alerts for persistent messages.
import { ShowToastEvent } from class="code-string">'lightning/platformShowToastEvent';
handleSuccess() {
this.dispatchEvent(new ShowToastEvent({
title: class="code-string">'Record Saved',
message: class="code-string">'Account has been updated successfully.',
variant: class="code-string">'success'
}));
}
Optimistic Updates
Update the UI immediately, then reconcile with the server response.
async handleToggleFavorite() {
const previousState = this._isFavorite;
this._isFavorite = !this._isFavorite; class="code-comment">// optimistic
try {
await toggleFavorite({ recordId: this.recordId });
} catch (error) {
this._isFavorite = previousState; class="code-comment">// rollback
this.dispatchEvent(new ShowToastEvent({
title: class="code-string">'Error',
message: class="code-string">'Could not update favorite status.',
variant: class="code-string">'error'
}));
}
}
Demo Polish Patterns
Patterns that make demos and PoCs feel production-ready with minimal effort.
Skeleton-to-Content Reveal
The most impressive loading pattern for demos — content appears to "paint in":
async connectedCallback() {
this.isLoading = true;
const data = await loadData();
class="code-comment">// Brief delay makes the skeleton visible (for demo effect)
class="code-comment">// Remove this delay in production
await new Promise(resolve => setTimeout(resolve, 600));
this._data = data;
this.isLoading = false;
}
Progressive Data Loading
Load sections independently so the page feels alive:
async connectedCallback() {
class="code-comment">// Fire all requests in parallel, render each as it arrives
this.loadMetrics();
this.loadTimeline();
this.loadRelatedRecords();
}
async loadMetrics() {
this.metricsLoading = true;
this.metrics = await getMetrics({ recordId: this.recordId });
this.metricsLoading = false;
}
async loadTimeline() {
this.timelineLoading = true;
this.timeline = await getTimeline({ recordId: this.recordId });
this.timelineLoading = false;
}
async loadRelatedRecords() {
this.relatedLoading = true;
this.relatedRecords = await getRelated({ recordId: this.recordId });
this.relatedLoading = false;
}
"Before and After" Pattern
Show the old way vs the new way in a single component:
<class="code-tag">template>
<class="code-tag">div class="before-after">
<class="code-tag">div class="before-after__panel">
<class="code-tag">div class="before-after__label before-after__label--before">Before</class="code-tag">div>
<class="code-tag">div class="before-after__content">
class="code-comment"><!-- Show the old, clunky process -->
<class="code-tag">p>15 clicks across 4 screens</class="code-tag">p>
<class="code-tag">p>Average time: 8 minutes</class="code-tag">p>
</class="code-tag">div>
</class="code-tag">div>
<class="code-tag">div class="before-after__divider">
<class="code-tag">lightning-icon icon-name="utility:forward" size="small"></class="code-tag">lightning-icon>
</class="code-tag">div>
<class="code-tag">div class="before-after__panel">
<class="code-tag">div class="before-after__label before-after__label--after">After</class="code-tag">div>
<class="code-tag">div class="before-after__content">
class="code-comment"><!-- Show the new, streamlined process -->
<class="code-tag">p>2 clicks, 1 screen</class="code-tag">p>
<class="code-tag">p>Average time: 30 seconds</class="code-tag">p>
</class="code-tag">div>
</class="code-tag">div>
</class="code-tag">div>
</class="code-tag">template>
Instant Feedback on Actions
For demos, make every action feel responsive:
async handleSave() {
class="code-comment">// Immediately show saving state
this.isSaving = true;
this.saveButtonLabel = class="code-string">'Saving...';
try {
await saveRecord({ record: this.record });
class="code-comment">// Brief success state before resetting
this.saveButtonLabel = class="code-string">'Saved!';
this.saveButtonVariant = class="code-string">'success';
this.dispatchEvent(new ShowToastEvent({
title: class="code-string">'Success',
message: class="code-string">'Record saved successfully.',
variant: class="code-string">'success'
}));
await new Promise(resolve => setTimeout(resolve, 1000));
} finally {
this.isSaving = false;
this.saveButtonLabel = class="code-string">'Save';
this.saveButtonVariant = class="code-string">'brand';
}
}
Micro-Interactions
Focus Rings
All interactive elements must have visible focus indicators.
: host {
--focus-ring-color: var(--slds-g-color-accent-1, #0176d3);
--focus-ring-offset: 2px;
}
.interactive: focus-visible {
outline: 2px solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
border-radius: var(--slds-g-radius-border-1, 0.125rem);
}
Hover States
.list-item {
transition: background-color 150ms ease;
}
.list-item: hover {
background-color: var(--slds-g-color-surface-3, #f3f3f3);
}
.list-item: active {
background-color: var(--slds-g-color-accent-container-1, #e5f0fb);
}
Transitions
Use short, purposeful transitions. Avoid decorative animation.
.expandable {
overflow: hidden;
max-height: 0;
transition: max-height 200ms ease-out;
}
.expandable--open {
max-height: 500px;
}