User feedback patterns including success/error messages, loading states, confirmations, and progress indicators. Use when implementing notifications, toasts, or status updates. (project)
User feedback mechanisms for communicating state changes, success, errors, and progress. This skill covers visual, auditory, and haptic feedback patterns.
material-symbols-v3: Icon names for status indicators (check_circle, error, warning)ux-iconography: Icon + text patterns for feedbackux-animation-motion: Anime.js animations for feedback effectsImmediate feedback near the action:
<button>Save</button>
<span class="inline-feedback" role="status" aria-live="polite">
Saved successfully
</span>
.inline-feedback {
font-size: var(--step--1);
color: var(--color-success);
opacity: 0;
transition: opacity 0.2s ease;
}
.inline-feedback.visible {
opacity: 1;
}
Non-blocking temporary messages as a web component:
class ToastContainer extends HTMLElement {
#container; // Direct reference - NO querySelector
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Build and store direct reference
this.#container = document.createElement('div');
this.#container.className = 'toast-container';
this.#container.setAttribute('part', 'container');
this.shadowRoot.appendChild(this.#container);
}
show(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.setAttribute('role', 'alert');
toast.setAttribute('part', 'toast');
toast.textContent = message;
this.#container.appendChild(toast); // Direct reference
// Auto-dismiss
setTimeout(() => {
toast.classList.add('exiting');
toast.addEventListener('animationend', () => toast.remove());
}, duration);
}
}
customElements.define('toast-container', ToastContainer);
.toast {
padding: var(--space-s) var(--space-m);
background: var(--theme-surface-variant);
border-radius: var(--space-2xs);
animation: slideIn 0.2s ease;
}
.toast-success {
border-left: 4px solid var(--color-success);
}
.toast-error {
border-left: 4px solid var(--color-error);
}
.toast.exiting {
animation: slideOut 0.2s ease forwards;
}
For destructive or important actions:
<dialog class="confirm-dialog">
<h2>Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<div class="dialog-actions">
<button class="btn-secondary">Cancel</button>
<button class="btn-danger">Delete</button>
</div>
</dialog>
For long-running operations:
<!-- Determinate progress -->
<progress value="60" max="100" aria-label="Upload progress">60%</progress>
<!-- Indeterminate/spinner -->
<div class="spinner" role="status" aria-label="Loading">
<span class="sr-only">Loading...</span>
</div>
/* Success color */
.success {
color: var(--color-success);
}
/* Success icon - use Material Symbol */
/* <span class="icon" aria-hidden="true">check_circle</span> */
.icon-success {
color: var(--color-success);
}
/* Success border */
.input-success {
border-color: var(--color-success);
}
import { successBounce, glow } from '../../utils/animations.js';
// On successful action
successBounce(element);
glow(element, { color: 'rgba(74, 222, 128, 0.6)' });
announce(message) {
// For screen readers
this.#announcer.textContent = '';
requestAnimationFrame(() => {
this.#announcer.textContent = message;
});
}
// Usage
this.announce('Word completed! 3 points earned.');
/* Error color */
.error {
color: var(--color-error);
}
/* Error state */
[aria-invalid="true"] {
border-color: var(--color-error);
}
/* Error message */
.error-message {
color: var(--color-error);
font-size: var(--step--1);
}
import { shake } from '../../utils/animations.js';
// On validation failure
shake(inputContainer, { intensity: 6 });
<div class="field">
<input aria-invalid="true" aria-describedby="error-1">
<span id="error-1" class="error-message" role="alert">
Please enter a valid email address
</span>
</div>
.button[aria-busy="true"] {
position: relative;
color: transparent;
pointer-events: none;
}
.button[aria-busy="true"]::after {
content: '';
position: absolute;
inset: 0;
margin: auto;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.skeleton {
background: linear-gradient(
90deg,
var(--theme-surface-variant) 25%,
var(--theme-surface) 50%,
var(--theme-surface-variant) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--space-2xs);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
<div class="loading-overlay" role="status">
<div class="spinner"></div>
<span class="sr-only">Loading game...</span>
</div>
.progress-bar {
height: 4px;
background: var(--theme-outline-variant);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--theme-primary);
transition: width 0.3s ease;
}
<ol class="steps" aria-label="Progress">
<li data-status="complete" aria-current="false">Step 1</li>
<li data-status="current" aria-current="step">Step 2</li>
<li data-status="pending" aria-current="false">Step 3</li>
</ol>
.steps [data-status="complete"] {
color: var(--color-success);
}
.steps [data-status="current"] {
color: var(--theme-primary);
font-weight: 600;
}
.steps [data-status="pending"] {
color: var(--theme-on-surface-variant);
}
.progress-circle {
--progress: 0;
width: 60px;
height: 60px;
border-radius: 50%;
background: conic-gradient(
var(--theme-primary) calc(var(--progress) * 1%),
var(--theme-outline-variant) 0
);
}
<div role="status" aria-live="polite" aria-atomic="true">
Score: 42 points
</div>
<div role="alert" aria-live="assertive">
Session expired. Please log in again.
</div>
class Announcer {
#region;
constructor() {
this.#region = document.createElement('div');
this.#region.setAttribute('role', 'status');
this.#region.setAttribute('aria-live', 'polite');
this.#region.setAttribute('aria-atomic', 'true');
this.#region.className = 'sr-only';
document.body.appendChild(this.#region);
}
announce(message, priority = 'polite') {
this.#region.setAttribute('aria-live', priority);
this.#region.textContent = '';
requestAnimationFrame(() => {
this.#region.textContent = message;
});
}
}
| Feedback Type | Duration | Use Case |
|---|---|---|
| Micro-animation | 100-200ms | Button press, toggle |
| State transition | 200-300ms | Page change, modal |
| Toast display | 3-5 seconds | Success message |
| Error display | Until dismissed | Validation error |
| Loading indicator | Immediate | Any async operation |
aria-busyprefers-reduced-motion