Charts & Dashboards
Build metric cards, KPI displays, progress bars, and sparklines. Turn data into something people actually understand.
Data first, decoration secondAccessible alwaysSLDS color semanticsResponsive
Skill Details
Install this skill
Versionv1.1.0AuthorJorge ArteagaLicenseMITSections10
Works with
Claude CodeCursorWindsurf
Build data-rich Lightning Web Components that tell a story with numbers. Every visualization uses SLDS 2 hooks, is accessible, and degrades gracefully.
Core Principles
1
Data first, decoration second — the number/trend is the hero, not the chart
2
Accessible always — every visual has a text alternative or data table fallback
3
SLDS color semantics — use feedback hooks (success/warning/error) for status, not arbitrary colors
4
Responsive — visualizations adapt to container width without JS
Metric Card Pattern
The most common dataviz component: a single KPI with context.
Visual Hierarchy
┌──────────────────────────────┐ │ Label (secondary text) │ ← what this measures │ VALUE (large, bold) │ ← the number │ ▲ +12% vs last period │ ← trend context │ ▔▔▔▁▁▔▔▔▔▁▔▔▔▔▔ │ ← sparkline (optional) └──────────────────────────────┘
HTML
<class="code-tag">template>
<class="code-tag">div class="metric-card">
<class="code-tag">span class="metric-label">{label}</class="code-tag">span>
<class="code-tag">span class="metric-value">{formattedValue}</class="code-tag">span>
<class="code-tag">div class="metric-trend" if:true={hasTrend}>
<class="code-tag">lightning-icon
icon-name={trendIcon}
size="xx-small"
class={trendClass}>
</class="code-tag">lightning-icon>
<class="code-tag">span class={trendClass}>{trendText}</class="code-tag">span>
</class="code-tag">div>
<class="code-tag">div class="metric-sparkline" if:true={hasSparkline}>
<class="code-tag">c-sparkline data={sparklineData} color={sparklineColor}></class="code-tag">c-sparkline>
</class="code-tag">div>
</class="code-tag">div>
</class="code-tag">template>
CSS
.metric-card {
display: flex;
flex-direction: column;
gap: var(--slds-g-spacing-1, 0.25rem);
padding: var(--slds-g-spacing-4, 1rem);
background: var(--slds-g-color-surface-1, #ffffff);
border: var(--slds-g-sizing-border-1) solid var(--slds-g-color-border-1, #e5e5e5);
border-radius: var(--slds-g-radius-border-3, 0.5rem);
}
.metric-label {
font-size: var(--slds-g-font-size-2, 0.75rem);
font-weight: var(--slds-g-font-weight-5, 500);
color: var(--slds-g-color-on-surface-2, #444444);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.metric-value {
font-size: var(--slds-g-font-size-9, 1.75rem);
font-weight: var(--slds-g-font-weight-7, 700);
color: var(--slds-g-color-on-surface-1, #181818);
line-height: var(--slds-g-line-height-1, 1);
}
.metric-trend {
display: flex;
align-items: center;
gap: var(--slds-g-spacing-1, 0.25rem);
font-size: var(--slds-g-font-size-2, 0.75rem);
font-weight: var(--slds-g-font-weight-5, 500);
}
.trend--positive { color: var(--slds-g-color-success-1, #2e844a); }
.trend--negative { color: var(--slds-g-color-error-1, #ea001e); }
.trend--neutral { color: var(--slds-g-color-on-surface-2, #444444); }
JavaScript — Number Formatting
get formattedValue() {
if (this.value == null) return class="code-string">'—';
if (this.format === class="code-string">'currency') {
return new Intl.NumberFormat(class="code-string">'en-US', {
style: class="code-string">'currency', currency: class="code-string">'USD',
minimumFractionDigits: 0, maximumFractionDigits: 0
}).format(this.value);
}
if (this.format === class="code-string">'percent') {
return new Intl.NumberFormat(class="code-string">'en-US', {
style: class="code-string">'percent', minimumFractionDigits: 1
}).format(this.value / 100);
}
if (Math.abs(this.value) >= 1e6) {
return (this.value / 1e6).toFixed(1) + class="code-string">'M';
}
if (Math.abs(this.value) >= 1e3) {
return (this.value / 1e3).toFixed(1) + class="code-string">'K';
}
return this.value.toLocaleString();
}
get trendIcon() {
if (this.trendValue > 0) return class="code-string">'utility:arrowup';
if (this.trendValue < 0) return class="code-string">'utility:arrowdown';
return class="code-string">'utility:dash';
}
get trendClass() {
if (this.trendValue > 0) return class="code-string">'trend--positive';
if (this.trendValue < 0) return class="code-string">'trend--negative';
return class="code-string">'trend--neutral';
}
Sparkline Pattern
A minimal inline chart using SVG polyline, no external libraries.
get sparklinePath() {
if (!this.data || this.data.length < 2) return class="code-string">'';
const width = 120;
const height = 32;
const max = Math.max(...this.data);
const min = Math.min(...this.data);
const range = max - min || 1;
const step = width / (this.data.length - 1);
return this.data
.map((val, i) => {
const x = i * step;
const y = height - ((val - min) / range) * height;
return class="code-string">`${x},${y}`;
})
.join(class="code-string">' ');
}
<class="code-tag">template>
<class="code-tag">svg class="sparkline" viewBox="0 0 120 32"
preserveAspectRatio="none"
role="img" aria-label={sparklineLabel}>
<class="code-tag">polyline
points={sparklinePath}
fill="none"
stroke={strokeColor}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
</class="code-tag">polyline>
</class="code-tag">svg>
</class="code-tag">template>
.sparkline {
width: 100%;
height: var(--slds-g-sizing-4, 2rem);
display: block;
}
Progress / Gauge Patterns
Linear Progress Bar
<class="code-tag">template>
<class="code-tag">div class="progress-bar" role="progressbar"
aria-valuenow={value} aria-valuemin="0" aria-valuemax="100"
aria-label={label}>
<class="code-tag">div class="progress-bar__fill" style={fillStyle}></class="code-tag">div>
</class="code-tag">div>
<class="code-tag">span class="progress-label">{value}%</class="code-tag">span>
</class="code-tag">template>
.progress-bar {
height: var(--slds-g-spacing-2, 0.5rem);
background: var(--slds-g-color-surface-container-2, #f3f3f3);
border-radius: var(--slds-g-radius-border-pill, 9999px);
overflow: hidden;
}
.progress-bar__fill {
height: 100%;
border-radius: var(--slds-g-radius-border-pill, 9999px);
transition: width 400ms ease;
}
get fillStyle() {
const color = this.value >= 80
? class="code-string">'var(--slds-g-color-success-1, #2e844a)'
: this.value >= 50
? class="code-string">'var(--slds-g-color-warning-1, #dd7a01)'
: class="code-string">'var(--slds-g-color-error-1, #ea001e)';
return class="code-string">`width: ${Math.min(100, Math.max(0, this.value))}%; background: ${color}`;
}
Radial Gauge (CSS conic-gradient)
.radial-gauge {
--gauge-value: 0;
--gauge-color: var(--slds-g-color-accent-1, #0176d3);
--gauge-bg: var(--slds-g-color-surface-container-2, #f3f3f3);
--gauge-size: 80px;
width: var(--gauge-size);
height: var(--gauge-size);
border-radius: var(--slds-g-radius-circle);
background: conic-gradient(
var(--gauge-color) calc(var(--gauge-value) * 1%),
var(--gauge-bg) 0
);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.radial-gauge__inner {
width: calc(var(--gauge-size) - 12px);
height: calc(var(--gauge-size) - 12px);
border-radius: var(--slds-g-radius-circle);
background: var(--slds-g-color-surface-1, #ffffff);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--slds-g-font-size-4, 0.875rem);
font-weight: var(--slds-g-font-weight-7, 700);
color: var(--slds-g-color-on-surface-1, #181818);
}
Color Semantics for Data
Always use SLDS feedback colors semantically. Never assign arbitrary colors to data states.
| Data Meaning | SLDS Hook | Use |
|---|---|---|
| Positive / on-track | --slds-g-color-success-1 | Revenue up, target met, healthy |
| Negative / at-risk | --slds-g-color-error-1 | Revenue down, overdue, critical |
| Caution / approaching limit | --slds-g-color-warning-1 | Nearing quota, expiring soon |
| Neutral / informational | --slds-g-color-accent-1 | Baseline, selected, highlighted |
| Inactive / not applicable | --slds-g-color-disabled-1 | No data, unavailable |
| Category | Points | Pass Criteria |
| Visual Hierarchy | 20 | Clear label/value/context ordering; value is prominent |
| SLDS Color Semantics | 20 | Feedback colors used correctly; no arbitrary colors for status |
| Accessibility | 20 | Text alternatives, aria attributes, data table fallbacks |
| Number Formatting | 15 | Proper locale formatting, abbreviations, currency/percent |
| Responsive Layout | 15 | Metrics reflow at narrow widths; no horizontal overflow |
| Performance | 10 | No external chart libraries; CSS/SVG only; efficient rendering |
| Skill | Relationship | |
| sf-lwc-design | Provides the SLDS 2 hooks used for all data viz colors and spacing | |
| sf-lwc-ux | Loading/empty/error states wrap around data viz components | |
| sf-lwc-styling | Utility classes used for layout composition around metrics | |
| sf-lwc-page-composition | Metric cards must work in App Builder column layouts | |
| sf-se-demo-scripts | Industry KPI templates feed directly into demo dashboards |
Navigate Data & Quality