Component Visual Patterns
World-class component visual recipes for systemprompt.io. Follow without exception when building, modifying, or reviewing component visuals.
Preamble — Skill Relationships
This skill provides visual design patterns for specific component types. It combines tokens, techniques, and interaction patterns into ready-to-use component recipes.
Complementary skills (do not duplicate their content):
| Skill | Covers | This Skill Adds |
|---|---|---|
frontend-coding-standards |
Web Component architecture, BEM .sp-* naming, event delegation, accessibility |
Visual styling for those components |
visual-design-system |
Design tokens, color system, typography scale | Token application in component context |
modern-css-patterns |
CSS techniques (container queries, grid, has(), nesting) | Technique composition into component recipes |
ux-interaction-patterns |
Animation, transitions, micro-interactions, UX flows | Static visual structure that animations enhance |
Rule: If a pattern is about structure/architecture, defer to frontend-coding-standards. If about tokens/color/type, defer to visual-design-system. If about CSS technique, defer to modern-css-patterns. If about motion/UX behavior, defer to ux-interaction-patterns. This skill is the visual assembly layer.
Branded Asymmetric Corners
systemprompt.io uses asymmetric border-radius as its visual signature. The top-right corner is approximately 1/4 of the other corners, creating a distinctive branded shape.
/* Corner tokens — use these, NEVER symmetric border-radius */
--sp-corners-sm: 6px 2px 6px 6px; /* small elements: badges, chips, tags */
--sp-corners-md: 12px 3px 12px 12px; /* cards, containers (default) */
--sp-corners-lg: 20px 5px 20px 20px; /* modals, dialogs, sheets */
Rule: Every card, container, modal, dialog, dropdown, and panel MUST use border-radius: var(--sp-corners-md) (or sm/lg as appropriate). Never use symmetric border-radius values. This is non-negotiable.
Card Patterns
Standard Card
The foundational content container. Image (optional), header, body, footer in vertical stack.
<div class="sp-card">
<div class="sp-card__media">
<img src="image.webp" alt="Descriptive text" class="sp-card__image" />
</div>
<div class="sp-card__header">
<h3 class="sp-card__title">Card Title</h3>
<span class="sp-card__badge">Label</span>
</div>
<div class="sp-card__body">
<p class="sp-card__text">Card description content.</p>
</div>
<div class="sp-card__footer">
<button class="sp-btn sp-btn--secondary">Cancel</button>
<button class="sp-btn sp-btn--primary">Action</button>
</div>
</div>
.sp-card {
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-sm);
overflow: hidden;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.sp-card:hover {
box-shadow: var(--sp-shadow-md);
transform: translateY(-1px);
}
.sp-card__media {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.sp-card__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.sp-card__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp-space-5) var(--sp-space-5) 0;
}
.sp-card__title {
font-size: var(--sp-font-size-lg);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text);
margin: 0;
}
.sp-card__body {
padding: var(--sp-space-3) var(--sp-space-5);
}
.sp-card__text {
color: var(--sp-color-text-secondary);
font-size: var(--sp-font-size-sm);
line-height: var(--sp-line-height-relaxed);
margin: 0;
}
.sp-card__footer {
display: flex;
justify-content: flex-end;
gap: var(--sp-space-3);
padding: 0 var(--sp-space-5) var(--sp-space-5);
}
Metric Card (KPI)
Compact card for key performance indicators. Large number dominates, delta indicator shows trend.
<div class="sp-metric-card">
<div class="sp-metric-card__header">
<span class="sp-metric-card__label">Monthly Revenue</span>
<span class="sp-metric-card__icon" aria-hidden="true">$</span>
</div>
<div class="sp-metric-card__value">
<span class="sp-metric-card__number">42,380</span>
<span class="sp-metric-card__unit">USD</span>
</div>
<div class="sp-metric-card__delta sp-metric-card__delta--up">
<span class="sp-metric-card__arrow" aria-hidden="true"></span>
<span class="sp-metric-card__percent">12.5%</span>
<span class="sp-metric-card__period">vs last month</span>
</div>
<svg class="sp-metric-card__sparkline" viewBox="0 0 100 30" preserveAspectRatio="none" aria-hidden="true">
<polyline points="0,25 15,20 30,22 45,15 60,18 75,8 100,5" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>
</div>
.sp-metric-card {
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-sm);
padding: var(--sp-space-5);
display: flex;
flex-direction: column;
gap: var(--sp-space-2);
}
.sp-metric-card__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.sp-metric-card__label {
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text-muted);
font-weight: var(--sp-font-weight-medium);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sp-metric-card__icon {
font-size: var(--sp-font-size-lg);
color: var(--sp-color-text-muted);
}
.sp-metric-card__value {
display: flex;
align-items: baseline;
gap: var(--sp-space-2);
}
.sp-metric-card__number {
font-size: var(--sp-font-size-3xl);
font-weight: var(--sp-font-weight-bold);
color: var(--sp-color-text);
line-height: 1;
}
.sp-metric-card__unit {
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text-muted);
}
.sp-metric-card__delta {
display: flex;
align-items: center;
gap: var(--sp-space-1);
font-size: var(--sp-font-size-sm);
}
.sp-metric-card__delta--up {
color: var(--sp-color-success);
}
.sp-metric-card__delta--down {
color: var(--sp-color-danger);
}
.sp-metric-card__delta--flat {
color: var(--sp-color-text-muted);
}
.sp-metric-card__arrow {
display: inline-block;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
}
.sp-metric-card__delta--up .sp-metric-card__arrow {
border-bottom: 5px solid currentColor;
}
.sp-metric-card__delta--down .sp-metric-card__arrow {
border-top: 5px solid currentColor;
}
.sp-metric-card__period {
color: var(--sp-color-text-muted);
}
.sp-metric-card__sparkline {
width: 100%;
height: 30px;
color: var(--sp-color-primary);
margin-top: var(--sp-space-2);
}
Bento Grid Layout
Varied-span grid for visual hierarchy. Featured items span multiple columns/rows.
<div class="sp-bento">
<div class="sp-bento__item sp-bento__item--featured">Featured</div>
<div class="sp-bento__item">Standard</div>
<div class="sp-bento__item">Standard</div>
<div class="sp-bento__item sp-bento__item--wide">Wide</div>
<div class="sp-bento__item">Standard</div>
<div class="sp-bento__item sp-bento__item--tall">Tall</div>
</div>
.sp-bento {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--sp-space-4);
}
.sp-bento__item {
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-sm);
padding: var(--sp-space-5);
min-height: 200px;
}
.sp-bento__item--featured {
grid-column: span 2;
grid-row: span 2;
}
.sp-bento__item--wide {
grid-column: span 2;
}
.sp-bento__item--tall {
grid-row: span 2;
}
/* Collapse spans on narrow viewports */
@media (max-width: 640px) {
.sp-bento__item--featured,
.sp-bento__item--wide {
grid-column: span 1;
}
.sp-bento__item--featured {
grid-row: span 1;
}
}
Horizontal Card
Side-by-side image + content. Stacks on narrow containers.
<div class="sp-card-horizontal">
<div class="sp-card-horizontal__media">
<img src="image.webp" alt="Description" class="sp-card-horizontal__image" />
</div>
<div class="sp-card-horizontal__content">
<h3 class="sp-card-horizontal__title">Horizontal Card</h3>
<p class="sp-card-horizontal__text">Content beside the image.</p>
<button class="sp-btn sp-btn--primary sp-btn--sm">Action</button>
</div>
</div>
.sp-card-horizontal {
container-type: inline-size;
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-sm);
overflow: hidden;
display: grid;
grid-template-columns: 240px 1fr;
}
.sp-card-horizontal__media {
aspect-ratio: 1;
overflow: hidden;
}
.sp-card-horizontal__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.sp-card-horizontal__content {
padding: var(--sp-space-5);
display: flex;
flex-direction: column;
gap: var(--sp-space-3);
}
.sp-card-horizontal__title {
font-size: var(--sp-font-size-lg);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text);
margin: 0;
}
.sp-card-horizontal__text {
color: var(--sp-color-text-secondary);
font-size: var(--sp-font-size-sm);
margin: 0;
flex: 1;
}
/* Stack on narrow containers */
@container (max-width: 480px) {
.sp-card-horizontal {
grid-template-columns: 1fr;
}
.sp-card-horizontal__media {
aspect-ratio: 16 / 9;
}
}
Table Patterns
Standard Data Table
Sticky header, row hover, sort indicators.
.sp-table {
width: 100%;
border-collapse: collapse;
font-size: var(--sp-font-size-sm);
}
.sp-table__header {
position: sticky;
top: 0;
z-index: 1;
}
.sp-table__header-cell {
padding: var(--sp-space-3) var(--sp-space-4);
text-align: left;
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text-muted);
background: var(--sp-color-surface-alt);
border-bottom: 2px solid var(--sp-color-border);
white-space: nowrap;
user-select: none;
cursor: pointer;
}
.sp-table__header-cell--sortable::after {
content: "";
display: inline-block;
width: 0;
height: 0;
margin-left: var(--sp-space-2);
vertical-align: middle;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid var(--sp-color-text-muted);
opacity: 0.3;
}
.sp-table__header-cell--asc::after {
border-top: none;
border-bottom: 5px solid var(--sp-color-primary);
opacity: 1;
}
.sp-table__header-cell--desc::after {
border-top: 5px solid var(--sp-color-primary);
opacity: 1;
}
.sp-table__row {
transition: background-color 0.15s ease;
}
.sp-table__row:hover {
background: var(--sp-color-surface-hover);
}
.sp-table__row:nth-child(even) {
background: var(--sp-color-surface-alt);
}
.sp-table__row:nth-child(even):hover {
background: var(--sp-color-surface-hover);
}
.sp-table__cell {
padding: var(--sp-space-3) var(--sp-space-4);
border-bottom: 1px solid var(--sp-color-border-light);
color: var(--sp-color-text);
vertical-align: middle;
}
Responsive Table
Collapse to card layout on narrow screens using data-label for mobile headers.
<table class="sp-table sp-table--responsive">
<thead class="sp-table__header">
<tr>
<th class="sp-table__header-cell">Name</th>
<th class="sp-table__header-cell">Status</th>
<th class="sp-table__header-cell">Amount</th>
</tr>
</thead>
<tbody>
<tr class="sp-table__row">
<td class="sp-table__cell" data-label="Name">Alice</td>
<td class="sp-table__cell" data-label="Status">Active</td>
<td class="sp-table__cell" data-label="Amount">$1,200</td>
</tr>
</tbody>
</table>
@media (max-width: 640px) {
.sp-table--responsive .sp-table__header {
display: none;
}
.sp-table--responsive .sp-table__row {
display: block;
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-sm);
margin-bottom: var(--sp-space-3);
padding: var(--sp-space-4);
}
.sp-table--responsive .sp-table__cell {
display: flex;
justify-content: space-between;
padding: var(--sp-space-2) 0;
border-bottom: 1px solid var(--sp-color-border-light);
}
.sp-table--responsive .sp-table__cell:last-child {
border-bottom: none;
}
.sp-table--responsive .sp-table__cell::before {
content: attr(data-label);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text-muted);
margin-right: var(--sp-space-4);
}
}
Expandable Rows
Click to reveal detail row with chevron rotation and left border accent.
.sp-table__row--expandable {
cursor: pointer;
}
.sp-table__row--expandable .sp-table__chevron {
display: inline-block;
width: 8px;
height: 8px;
border-right: 2px solid var(--sp-color-text-muted);
border-bottom: 2px solid var(--sp-color-text-muted);
transform: rotate(-45deg);
transition: transform 0.2s ease;
}
.sp-table__row--expandable[aria-expanded="true"] .sp-table__chevron {
transform: rotate(45deg);
}
.sp-table__detail {
display: none;
background: var(--sp-color-surface-alt);
border-left: 3px solid var(--sp-color-primary);
}
.sp-table__row--expandable[aria-expanded="true"] + .sp-table__detail {
display: table-row;
}
.sp-table__detail-content {
padding: var(--sp-space-4) var(--sp-space-5);
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text-secondary);
}
Form Patterns
Input Group
Label above (never floating), help text, error message with clear visual states.
<div class="sp-field">
<label class="sp-field__label" for="email">Email Address</label>
<input class="sp-field__input" type="email" id="email" aria-describedby="email-help email-error" />
<span class="sp-field__help" id="email-help">We'll never share your email.</span>
<span class="sp-field__error" id="email-error" role="alert" hidden>Please enter a valid email address.</span>
</div>
.sp-field {
display: flex;
flex-direction: column;
gap: var(--sp-space-1);
}
.sp-field__label {
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-medium);
color: var(--sp-color-text);
}
.sp-field__input {
padding: var(--sp-space-3) var(--sp-space-4);
border: 1px solid var(--sp-color-border);
border-radius: var(--sp-corners-sm);
font-size: var(--sp-font-size-base);
color: var(--sp-color-text);
background: var(--sp-color-surface);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.sp-field__input:focus {
outline: none;
border-color: var(--sp-color-primary);
box-shadow: 0 0 0 3px var(--sp-color-primary-alpha);
}
.sp-field__input[aria-invalid="true"] {
border-color: var(--sp-color-danger);
box-shadow: 0 0 0 3px var(--sp-color-danger-alpha);
}
.sp-field__input--success {
border-color: var(--sp-color-success);
box-shadow: 0 0 0 3px var(--sp-color-success-alpha);
}
.sp-field__help {
font-size: var(--sp-font-size-xs);
color: var(--sp-color-text-muted);
}
.sp-field__error {
font-size: var(--sp-font-size-xs);
color: var(--sp-color-danger);
display: flex;
align-items: center;
gap: var(--sp-space-1);
}
.sp-field__error[hidden] {
display: none;
}
Inline Validation
Real-time feedback after first blur. Success shows green border + check, error shows red border + message slide.
.sp-field__input:not(:placeholder-shown):valid {
border-color: var(--sp-color-success);
}
.sp-field__input:not(:placeholder-shown):invalid {
border-color: var(--sp-color-danger);
}
.sp-field__validation-icon {
position: absolute;
right: var(--sp-space-3);
top: 50%;
transform: translateY(-50%);
font-size: var(--sp-font-size-base);
opacity: 0;
transition: opacity 0.2s ease;
}
.sp-field__input--success ~ .sp-field__validation-icon--check {
opacity: 1;
color: var(--sp-color-success);
}
.sp-field__input[aria-invalid="true"] ~ .sp-field__validation-icon--x {
opacity: 1;
color: var(--sp-color-danger);
}
.sp-field__error {
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease;
}
.sp-field__input[aria-invalid="true"] ~ .sp-field__error {
max-height: 3em;
}
Multi-Step Form
Step indicator with numbered circles and connecting lines.
<div class="sp-stepper" role="navigation" aria-label="Form progress">
<ol class="sp-stepper__list">
<li class="sp-stepper__step sp-stepper__step--completed">
<span class="sp-stepper__circle" aria-hidden="true">✓</span>
<span class="sp-stepper__label">Account</span>
</li>
<li class="sp-stepper__step sp-stepper__step--active" aria-current="step">
<span class="sp-stepper__circle" aria-hidden="true">2</span>
<span class="sp-stepper__label">Profile</span>
</li>
<li class="sp-stepper__step">
<span class="sp-stepper__circle" aria-hidden="true">3</span>
<span class="sp-stepper__label">Confirm</span>
</li>
</ol>
</div>
.sp-stepper__list {
display: flex;
justify-content: center;
list-style: none;
padding: 0;
margin: 0 0 var(--sp-space-6);
counter-reset: step;
}
.sp-stepper__step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 1;
}
/* Connecting line */
.sp-stepper__step:not(:last-child)::after {
content: "";
position: absolute;
top: 16px;
left: calc(50% + 20px);
width: calc(100% - 40px);
height: 2px;
background: var(--sp-color-border);
}
.sp-stepper__step--completed:not(:last-child)::after {
background: var(--sp-color-primary);
}
.sp-stepper__circle {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--sp-color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text-muted);
background: var(--sp-color-surface);
position: relative;
z-index: 1;
}
.sp-stepper__step--active .sp-stepper__circle {
border-color: var(--sp-color-primary);
background: var(--sp-color-primary);
color: var(--sp-color-on-primary);
}
.sp-stepper__step--completed .sp-stepper__circle {
border-color: var(--sp-color-primary);
background: var(--sp-color-primary);
color: var(--sp-color-on-primary);
}
.sp-stepper__label {
margin-top: var(--sp-space-2);
font-size: var(--sp-font-size-xs);
color: var(--sp-color-text-muted);
}
.sp-stepper__step--active .sp-stepper__label {
color: var(--sp-color-text);
font-weight: var(--sp-font-weight-medium);
}
Accessible Error Summary
Top-of-form error summary with links to each invalid field.
<div class="sp-error-summary" role="alert" aria-labelledby="error-heading">
<h2 class="sp-error-summary__title" id="error-heading">There are 2 errors in this form</h2>
<ul class="sp-error-summary__list">
<li class="sp-error-summary__item">
<a class="sp-error-summary__link" href="#email">Email address is required</a>
</li>
<li class="sp-error-summary__item">
<a class="sp-error-summary__link" href="#password">Password must be at least 8 characters</a>
</li>
</ul>
</div>
.sp-error-summary {
border-radius: var(--sp-corners-md);
border: 1px solid var(--sp-color-danger);
border-left: 4px solid var(--sp-color-danger);
background: var(--sp-color-danger-surface);
padding: var(--sp-space-4) var(--sp-space-5);
margin-bottom: var(--sp-space-5);
}
.sp-error-summary__title {
font-size: var(--sp-font-size-base);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-danger);
margin: 0 0 var(--sp-space-3);
}
.sp-error-summary__list {
margin: 0;
padding-left: var(--sp-space-5);
}
.sp-error-summary__item {
margin-bottom: var(--sp-space-1);
}
.sp-error-summary__link {
color: var(--sp-color-danger);
text-decoration: underline;
font-size: var(--sp-font-size-sm);
}
.sp-error-summary__link:hover {
text-decoration: none;
}
Navigation Patterns
Top Navigation Bar
Logo left, nav center/right, actions far right. Active state uses bottom border accent.
.sp-navbar {
display: flex;
align-items: center;
height: 60px;
padding: 0 var(--sp-space-5);
background: var(--sp-color-surface);
border-bottom: 1px solid var(--sp-color-border-light);
position: sticky;
top: 0;
z-index: var(--sp-z-sticky);
}
.sp-navbar__brand {
display: flex;
align-items: center;
gap: var(--sp-space-3);
margin-right: auto;
}
.sp-navbar__nav {
display: flex;
align-items: center;
gap: var(--sp-space-1);
list-style: none;
margin: 0;
padding: 0;
height: 100%;
}
.sp-navbar__link {
display: flex;
align-items: center;
height: 100%;
padding: 0 var(--sp-space-4);
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-medium);
color: var(--sp-color-text-secondary);
text-decoration: none;
border-bottom: 3px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
}
.sp-navbar__link:hover {
color: var(--sp-color-text);
}
.sp-navbar__link--active {
color: var(--sp-color-primary);
border-bottom-color: var(--sp-color-primary);
}
.sp-navbar__actions {
display: flex;
align-items: center;
gap: var(--sp-space-3);
margin-left: auto;
}
/* Mobile hamburger */
.sp-navbar__toggle {
display: none;
background: none;
border: none;
padding: var(--sp-space-2);
cursor: pointer;
}
@media (max-width: 768px) {
.sp-navbar__nav {
position: fixed;
top: 60px;
left: 0;
bottom: 0;
width: 280px;
flex-direction: column;
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-lg);
padding: var(--sp-space-4);
transform: translateX(-100%);
transition: transform 0.3s ease;
height: auto;
gap: 0;
}
.sp-navbar__nav--open {
transform: translateX(0);
}
.sp-navbar__link {
height: auto;
padding: var(--sp-space-3) var(--sp-space-4);
border-bottom: none;
border-left: 3px solid transparent;
border-radius: var(--sp-corners-sm);
}
.sp-navbar__link--active {
border-left-color: var(--sp-color-primary);
background: var(--sp-color-primary-alpha);
}
.sp-navbar__toggle {
display: block;
}
}
Sidebar Navigation
Collapsible between expanded (240px) and icon-only (64px).
.sp-sidebar {
width: 260px;
min-height: 100vh;
background: var(--sp-color-surface);
border-right: 1px solid var(--sp-color-border-light);
padding: var(--sp-space-4);
display: flex;
flex-direction: column;
gap: var(--sp-space-1);
transition: width 0.2s ease;
overflow: hidden;
}
.sp-sidebar--collapsed {
width: 64px;
padding: var(--sp-space-4) var(--sp-space-2);
}
.sp-sidebar__link {
display: flex;
align-items: center;
gap: var(--sp-space-3);
padding: var(--sp-space-3) var(--sp-space-4);
border-radius: var(--sp-corners-sm);
color: var(--sp-color-text-secondary);
text-decoration: none;
font-size: var(--sp-font-size-sm);
white-space: nowrap;
transition: background 0.15s ease, color 0.15s ease;
}
.sp-sidebar__link:hover {
background: var(--sp-color-surface-hover);
color: var(--sp-color-text);
}
.sp-sidebar__link--active {
background: var(--sp-color-primary-alpha);
color: var(--sp-color-primary);
border-left: 3px solid var(--sp-color-primary);
}
.sp-sidebar__link-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
}
.sp-sidebar--collapsed .sp-sidebar__link-text {
display: none;
}
.sp-sidebar--collapsed .sp-sidebar__link {
justify-content: center;
padding: var(--sp-space-3);
}
.sp-sidebar__section-label {
font-size: var(--sp-font-size-xs);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--sp-space-4) var(--sp-space-4) var(--sp-space-2);
}
.sp-sidebar--collapsed .sp-sidebar__section-label {
display: none;
}
Breadcrumbs
Separator-divided path with truncation for deep nesting.
.sp-breadcrumbs {
display: flex;
align-items: center;
gap: var(--sp-space-2);
font-size: var(--sp-font-size-sm);
padding: var(--sp-space-3) 0;
overflow: hidden;
}
.sp-breadcrumbs__item {
display: flex;
align-items: center;
gap: var(--sp-space-2);
white-space: nowrap;
}
.sp-breadcrumbs__link {
color: var(--sp-color-text-muted);
text-decoration: none;
transition: color 0.15s ease;
}
.sp-breadcrumbs__link:hover {
color: var(--sp-color-primary);
text-decoration: underline;
}
.sp-breadcrumbs__separator {
color: var(--sp-color-text-muted);
font-size: var(--sp-font-size-xs);
}
.sp-breadcrumbs__current {
color: var(--sp-color-text);
font-weight: var(--sp-font-weight-medium);
}
/* Truncation for deep paths */
.sp-breadcrumbs__ellipsis {
color: var(--sp-color-text-muted);
cursor: pointer;
padding: var(--sp-space-1) var(--sp-space-2);
border-radius: var(--sp-corners-sm);
}
.sp-breadcrumbs__ellipsis:hover {
background: var(--sp-color-surface-hover);
}
Tabs
Bottom border sliding indicator with scrollable overflow.
.sp-tabs {
position: relative;
border-bottom: 1px solid var(--sp-color-border-light);
overflow-x: auto;
scrollbar-width: none;
}
.sp-tabs::-webkit-scrollbar {
display: none;
}
.sp-tabs__list {
display: flex;
gap: var(--sp-space-1);
list-style: none;
margin: 0;
padding: 0;
}
.sp-tabs__tab {
padding: var(--sp-space-3) var(--sp-space-4);
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-medium);
color: var(--sp-color-text-secondary);
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
white-space: nowrap;
transition: color 0.15s ease, border-color 0.15s ease;
}
.sp-tabs__tab:hover {
color: var(--sp-color-text);
}
.sp-tabs__tab--active {
color: var(--sp-color-primary);
border-bottom-color: var(--sp-color-primary);
}
/* Fade edges for scroll indication */
.sp-tabs--scrollable::before,
.sp-tabs--scrollable::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 40px;
pointer-events: none;
z-index: 1;
}
.sp-tabs--scrollable::before {
left: 0;
background: linear-gradient(to right, var(--sp-color-surface), transparent);
}
.sp-tabs--scrollable::after {
right: 0;
background: linear-gradient(to left, var(--sp-color-surface), transparent);
}
Pagination
Current page highlighted, prev/next, ellipsis for long ranges.
.sp-pagination {
display: flex;
align-items: center;
gap: var(--sp-space-1);
font-size: var(--sp-font-size-sm);
}
.sp-pagination__btn {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--sp-space-2);
border: 1px solid var(--sp-color-border);
border-radius: var(--sp-corners-sm);
background: var(--sp-color-surface);
color: var(--sp-color-text);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.sp-pagination__btn:hover:not(:disabled) {
background: var(--sp-color-surface-hover);
border-color: var(--sp-color-primary);
}
.sp-pagination__btn--active {
background: var(--sp-color-primary);
border-color: var(--sp-color-primary);
color: var(--sp-color-on-primary);
font-weight: var(--sp-font-weight-semibold);
}
.sp-pagination__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.sp-pagination__ellipsis {
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: var(--sp-color-text-muted);
}
Dashboard Layout Patterns
Scanning Patterns
F-pattern (data-heavy dashboards): Users scan left-to-right across the top (KPIs), then down the left edge (nav/labels), with decreasing attention rightward. Place critical KPIs top-left, secondary metrics across the top row, and detail tables below.
Z-pattern (simpler dashboards): Users scan top-left to top-right, diagonally to bottom-left, then across to bottom-right. Place the logo/title top-left, primary action top-right, context bottom-left, CTA bottom-right.
Dashboard Grid
Named grid areas for semantic layout.
.sp-dashboard {
display: grid;
grid-template-areas:
"kpi kpi kpi"
"chart chart sidebar"
"table table sidebar";
grid-template-columns: 1fr 1fr 300px;
grid-template-rows: auto 1fr 1fr;
gap: var(--sp-space-4);
padding: var(--sp-space-4);
min-height: 100vh;
}
.sp-dashboard__kpi { grid-area: kpi; }
.sp-dashboard__chart { grid-area: chart; }
.sp-dashboard__sidebar { grid-area: sidebar; }
.sp-dashboard__table { grid-area: table; }
/* Responsive: single column on mobile */
@media (max-width: 768px) {
.sp-dashboard {
grid-template-areas:
"kpi"
"chart"
"sidebar"
"table";
grid-template-columns: 1fr;
grid-template-rows: auto;
}
}
KPI Row
3-5 metric cards in equal columns. Never exceed 5 -- cognitive overload threshold.
<div class="sp-kpi-row">
<div class="sp-metric-card"><!-- ... --></div>
<div class="sp-metric-card"><!-- ... --></div>
<div class="sp-metric-card"><!-- ... --></div>
<div class="sp-metric-card"><!-- ... --></div>
</div>
.sp-kpi-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--sp-space-4);
}
/* Cap at 5 columns */
@media (min-width: 1200px) {
.sp-kpi-row {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
max-width: 1400px;
}
}
Data Density Guidelines
| Density | Line Height | Cell Padding | Font Size | Use Case |
|---|---|---|---|---|
| Low | 1.8 | --sp-space-4 |
--sp-font-size-base |
Consumer dashboards, onboarding |
| Medium | 1.6 | --sp-space-3 |
--sp-font-size-sm |
Business dashboards (default) |
| High | 1.4 | --sp-space-2 |
--sp-font-size-xs |
Analyst tools, data exploration |
.sp-density--low { --sp-density-padding: var(--sp-space-4); --sp-density-font: var(--sp-font-size-base); line-height: 1.8; }
.sp-density--medium { --sp-density-padding: var(--sp-space-3); --sp-density-font: var(--sp-font-size-sm); line-height: 1.6; }
.sp-density--high { --sp-density-padding: var(--sp-space-2); --sp-density-font: var(--sp-font-size-xs); line-height: 1.4; }
Data Visualization Helpers
Sparkline (SVG)
Inline SVG with gradient fill beneath the line.
<svg class="sp-sparkline" viewBox="0 0 100 30" preserveAspectRatio="none" aria-hidden="true" role="img">
<defs>
<linearGradient id="sp-spark-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--sp-color-primary)" stop-opacity="0.2" />
<stop offset="100%" stop-color="var(--sp-color-primary)" stop-opacity="0" />
</linearGradient>
</defs>
<polygon points="0,30 0,25 15,20 30,22 45,15 60,18 75,8 100,5 100,30" fill="url(#sp-spark-grad)" />
<polyline points="0,25 15,20 30,22 45,15 60,18 75,8 100,5" fill="none" stroke="var(--sp-color-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
.sp-sparkline {
width: 100%;
height: 30px;
display: block;
}
Progress Bar
CSS-only animated fill with color variants.
<div class="sp-progress" role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress">
<div class="sp-progress__fill" style="width: 65%"></div>
</div>
.sp-progress {
height: 8px;
border-radius: 999px;
background: var(--sp-color-surface-alt);
overflow: hidden;
}
.sp-progress__fill {
height: 100%;
border-radius: 999px;
background: var(--sp-color-primary);
transition: width 0.4s ease;
}
.sp-progress--success .sp-progress__fill { background: var(--sp-color-success); }
.sp-progress--warning .sp-progress__fill { background: var(--sp-color-warning); }
.sp-progress--danger .sp-progress__fill { background: var(--sp-color-danger); }
/* Indeterminate animation */
.sp-progress--indeterminate .sp-progress__fill {
width: 30%;
animation: sp-progress-slide 1.5s ease-in-out infinite;
}
@keyframes sp-progress-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
Status Indicator
Colored dot with optional pulse for live states.
<span class="sp-status sp-status--active">
<span class="sp-status__dot" aria-hidden="true"></span>
<span class="sp-status__label">Active</span>
</span>
.sp-status {
display: inline-flex;
align-items: center;
gap: var(--sp-space-2);
font-size: var(--sp-font-size-sm);
}
.sp-status__dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.sp-status--active .sp-status__dot { background: var(--sp-color-success); }
.sp-status--warning .sp-status__dot { background: var(--sp-color-warning); }
.sp-status--error .sp-status__dot { background: var(--sp-color-danger); }
.sp-status--idle .sp-status__dot { background: var(--sp-color-text-muted); }
/* Pulse for live */
.sp-status--live .sp-status__dot {
animation: sp-pulse 2s ease-in-out infinite;
}
@keyframes sp-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
Trend Arrow
Directional indicator using CSS triangles.
.sp-trend {
display: inline-flex;
align-items: center;
gap: var(--sp-space-1);
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-medium);
}
.sp-trend__arrow {
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.sp-trend--up {
color: var(--sp-color-success);
}
.sp-trend--up .sp-trend__arrow {
border-bottom: 6px solid currentColor;
}
.sp-trend--down {
color: var(--sp-color-danger);
}
.sp-trend--down .sp-trend__arrow {
border-top: 6px solid currentColor;
}
.sp-trend--flat {
color: var(--sp-color-text-muted);
}
.sp-trend--flat .sp-trend__arrow {
width: 10px;
height: 2px;
background: currentColor;
border: none;
}
Empty State Patterns
Centered layout: icon, heading, description, CTA. Used when a section has no data yet.
<div class="sp-empty-state">
<div class="sp-empty-state__icon" aria-hidden="true">
<svg width="64" height="64" viewBox="0 0 64 64">
<!-- illustration SVG -->
</svg>
</div>
<h3 class="sp-empty-state__title">No projects yet</h3>
<p class="sp-empty-state__description">Create your first project to get started building with systemprompt.</p>
<button class="sp-btn sp-btn--primary">Create Project</button>
</div>
.sp-empty-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--sp-space-8) var(--sp-space-5);
max-width: 400px;
margin: 0 auto;
}
.sp-empty-state__icon {
width: 120px;
max-width: 200px;
margin-bottom: var(--sp-space-5);
color: var(--sp-color-text-muted);
opacity: 0.6;
}
.sp-empty-state__icon svg {
width: 100%;
height: auto;
}
.sp-empty-state__title {
font-size: var(--sp-font-size-lg);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text);
margin: 0 0 var(--sp-space-2);
}
.sp-empty-state__description {
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text-muted);
line-height: var(--sp-line-height-relaxed);
margin: 0 0 var(--sp-space-5);
}
Feedback Patterns
Toast Notification
Bottom-right stacked, auto-dismiss. Info/success dismiss in 5s, errors persist.
<div class="sp-toast-container" aria-live="polite" aria-relevant="additions">
<div class="sp-toast sp-toast--success" role="status">
<span class="sp-toast__icon" aria-hidden="true">✓</span>
<div class="sp-toast__content">
<span class="sp-toast__message">Settings saved successfully.</span>
<button class="sp-toast__action">Undo</button>
</div>
<button class="sp-toast__close" aria-label="Dismiss notification">×</button>
</div>
</div>
.sp-toast-container {
position: fixed;
bottom: var(--sp-space-5);
right: var(--sp-space-5);
z-index: var(--sp-z-toast);
display: flex;
flex-direction: column-reverse;
gap: var(--sp-space-3);
max-width: 420px;
}
.sp-toast {
display: flex;
align-items: flex-start;
gap: var(--sp-space-3);
padding: var(--sp-space-4);
border-radius: var(--sp-corners-md);
background: var(--sp-color-surface);
box-shadow: var(--sp-shadow-lg);
animation: sp-toast-in 0.3s ease forwards;
}
.sp-toast--exiting {
animation: sp-toast-out 0.2s ease forwards;
}
@keyframes sp-toast-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes sp-toast-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.sp-toast__icon {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--sp-font-size-sm);
}
.sp-toast--success .sp-toast__icon { color: var(--sp-color-success); }
.sp-toast--error .sp-toast__icon { color: var(--sp-color-danger); }
.sp-toast--warning .sp-toast__icon { color: var(--sp-color-warning); }
.sp-toast--info .sp-toast__icon { color: var(--sp-color-info); }
.sp-toast__content {
flex: 1;
}
.sp-toast__message {
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text);
}
.sp-toast__action {
background: none;
border: none;
color: var(--sp-color-primary);
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-medium);
cursor: pointer;
padding: 0;
margin-left: var(--sp-space-2);
}
.sp-toast__close {
background: none;
border: none;
color: var(--sp-color-text-muted);
font-size: var(--sp-font-size-lg);
cursor: pointer;
padding: 0;
line-height: 1;
}
Inline Alert
Full-width alert with left border accent and tinted background.
<div class="sp-alert sp-alert--warning" role="alert">
<span class="sp-alert__icon" aria-hidden="true">⚠</span>
<div class="sp-alert__content">
<strong class="sp-alert__title">Rate limit approaching</strong>
<p class="sp-alert__message">You have used 90% of your API quota for this month.</p>
</div>
</div>
.sp-alert {
display: flex;
align-items: flex-start;
gap: var(--sp-space-3);
padding: var(--sp-space-4) var(--sp-space-5);
border-radius: var(--sp-corners-sm);
border-left: 4px solid;
}
.sp-alert--info {
background: var(--sp-color-info-surface);
border-left-color: var(--sp-color-info);
}
.sp-alert--success {
background: var(--sp-color-success-surface);
border-left-color: var(--sp-color-success);
}
.sp-alert--warning {
background: var(--sp-color-warning-surface);
border-left-color: var(--sp-color-warning);
}
.sp-alert--error {
background: var(--sp-color-danger-surface);
border-left-color: var(--sp-color-danger);
}
.sp-alert__icon {
flex-shrink: 0;
font-size: var(--sp-font-size-lg);
}
.sp-alert--info .sp-alert__icon { color: var(--sp-color-info); }
.sp-alert--success .sp-alert__icon { color: var(--sp-color-success); }
.sp-alert--warning .sp-alert__icon { color: var(--sp-color-warning); }
.sp-alert--error .sp-alert__icon { color: var(--sp-color-danger); }
.sp-alert__title {
font-size: var(--sp-font-size-sm);
font-weight: var(--sp-font-weight-semibold);
color: var(--sp-color-text);
display: block;
margin-bottom: var(--sp-space-1);
}
.sp-alert__message {
font-size: var(--sp-font-size-sm);
color: var(--sp-color-text-secondary);
margin: 0;
}
Confirmation Dialog
Centered modal with backdrop. Uses branded --sp-corners-lg for the dialog container.
<div class="sp-dialog-backdrop" aria-hidden="true"></div>
<dialog class="sp-dialog" role="alertdialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<h2 class="sp-dialog__title" id="dialog-title">Del
…(truncated)