Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
User feedback components including progress indicators, spinners, snackbars, and tooltips for communicating system state and user actions. These components provide visual feedback and temporary notifications to enhance user experience.
Temporary notification component that appears at the bottom of the screen with optional actions.
/**
* Material Design snackbar component
* CSS Class: mdl-js-snackbar
* Widget: true
*/
interface MaterialSnackbar {
/**
* Show snackbar with configuration
* @param data - Configuration object for the snackbar
*/
showSnackbar(data: SnackbarData): void;
}
/**
* Configuration object for snackbar display
*/
interface SnackbarData {
/** Text message to display (required) */
message: string;
/** Optional action button text */
actionText?: string;
/** Optional action click handler function */
actionHandler?: () => void;
/** Optional timeout in milliseconds (default: 2750) */
timeout?: number;
}HTML Structure:
<!-- Basic snackbar container -->
<div id="demo-snackbar-example" class="mdl-js-snackbar mdl-snackbar">
<div class="mdl-snackbar__text"></div>
<button class="mdl-snackbar__action" type="button"></button>
</div>Usage Examples:
// Access snackbar instance
const snackbarContainer = document.querySelector('#demo-snackbar-example');
const snackbar = snackbarContainer.MaterialSnackbar;
// Simple message
snackbar.showSnackbar({
message: 'Hello World!'
});
// Message with action
snackbar.showSnackbar({
message: 'Email sent',
actionText: 'Undo',
actionHandler: function() {
console.log('Undo clicked');
undoEmailSend();
}
});
// Custom timeout
snackbar.showSnackbar({
message: 'This will disappear quickly',
timeout: 1000
});
// Queue multiple messages
function showMultipleMessages() {
snackbar.showSnackbar({
message: 'First message',
timeout: 2000
});
setTimeout(() => {
snackbar.showSnackbar({
message: 'Second message',
timeout: 2000
});
}, 2500);
}
// Common usage patterns
function showSuccessMessage(message) {
snackbar.showSnackbar({
message: message,
timeout: 3000
});
}
function showUndoableAction(message, undoCallback) {
snackbar.showSnackbar({
message: message,
actionText: 'Undo',
actionHandler: undoCallback,
timeout: 5000 // Longer timeout for actionable messages
});
}
// Example usage in forms
document.querySelector('#save-button').addEventListener('click', async () => {
try {
await saveData();
showSuccessMessage('Data saved successfully');
} catch (error) {
snackbar.showSnackbar({
message: 'Failed to save data',
actionText: 'Retry',
actionHandler: () => {
document.querySelector('#save-button').click();
}
});
}
});Linear progress indicator for showing task completion or loading states.
/**
* Material Design progress component
* CSS Class: mdl-js-progress
* Widget: true
*/
interface MaterialProgress {
/**
* Set progress value
* @param value - Progress value between 0 and 100
*/
setProgress(value: number): void;
/**
* Set buffer value for buffered progress
* @param value - Buffer value between 0 and 100
*/
setBuffer(value: number): void;
}
// Note: MaterialProgress automatically creates internal DOM structure
// with .mdl-progress__progressbar, .mdl-progress__bufferbar, and
// .mdl-progress__auxbar elements for rendering the progress visualizationHTML Structure:
<!-- Basic progress bar -->
<div id="p1" class="mdl-progress mdl-js-progress"></div>
<!-- Progress bar with initial value -->
<div id="p2" class="mdl-progress mdl-js-progress"
data-upgraded="true" style="width: 250px;"></div>
<!-- Indeterminate progress (for unknown duration) -->
<div id="p3" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>Usage Examples:
// Access progress instance
const progressBar = document.querySelector('#p1').MaterialProgress;
// Set progress value
progressBar.setProgress(65);
// Animate progress
function animateProgress(targetValue) {
let currentValue = 0;
const increment = targetValue / 100;
const interval = setInterval(() => {
currentValue += increment;
progressBar.setProgress(Math.min(currentValue, targetValue));
if (currentValue >= targetValue) {
clearInterval(interval);
}
}, 10);
}
// Usage with file upload
function handleFileUpload(file) {
const progressBar = document.querySelector('#upload-progress').MaterialProgress;
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
progressBar.setProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
progressBar.setProgress(100);
showSuccessMessage('Upload complete');
});
// Upload file
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/upload');
xhr.send(formData);
}
// Buffered progress (for streaming content)
function updateBufferedProgress(loaded, buffered, total) {
const progressBar = document.querySelector('#stream-progress').MaterialProgress;
const loadedPercent = (loaded / total) * 100;
const bufferedPercent = (buffered / total) * 100;
progressBar.setProgress(loadedPercent);
progressBar.setBuffer(bufferedPercent);
}Circular loading spinner for indicating ongoing operations.
/**
* Material Design spinner component
* CSS Class: mdl-js-spinner
* Widget: true
*/
interface MaterialSpinner {
/** Start the spinner animation */
start(): void;
/** Stop the spinner animation */
stop(): void;
/**
* Create a spinner layer with specified index
* @param index - Index of the layer to be created (1-4)
*/
createLayer(index: number): void;
}HTML Structure:
<!-- Basic spinner -->
<div class="mdl-spinner mdl-js-spinner"></div>
<!-- Active spinner -->
<div class="mdl-spinner mdl-js-spinner is-active"></div>
<!-- Single color spinner -->
<div class="mdl-spinner mdl-js-spinner mdl-spinner--single-color"></div>Usage Examples:
// Access spinner instance
const spinner = document.querySelector('.mdl-js-spinner').MaterialSpinner;
// Start/stop spinner
spinner.start();
spinner.stop();
// Usage with async operations
async function performAsyncOperation() {
const spinner = document.querySelector('#loading-spinner').MaterialSpinner;
spinner.start();
try {
const result = await fetchData();
return result;
} finally {
spinner.stop();
}
}
// Button with loading state
function setupLoadingButton(buttonSelector, spinnerSelector) {
const button = document.querySelector(buttonSelector);
const spinner = document.querySelector(spinnerSelector).MaterialSpinner;
button.addEventListener('click', async () => {
button.disabled = true;
spinner.start();
try {
await performLongOperation();
showSuccessMessage('Operation completed');
} catch (error) {
showErrorMessage('Operation failed');
} finally {
spinner.stop();
button.disabled = false;
}
});
}Contextual information component that appears on hover or focus.
/**
* Material Design tooltip component
* CSS Class: mdl-js-tooltip
* Widget: false
*/
interface MaterialTooltip {
// No public methods - behavior is entirely automatic
// Tooltips show on hover/focus and hide on mouse leave/blur
}HTML Structure:
<!-- Basic tooltip -->
<div id="tt1" class="icon material-icons">add</div>
<div class="mdl-tooltip" for="tt1">Follow</div>
<!-- Large tooltip -->
<div id="tt2" class="icon material-icons">print</div>
<div class="mdl-tooltip mdl-tooltip--large" for="tt2">
Print
</div>
<!-- Left-aligned tooltip -->
<div id="tt3" class="icon material-icons">cloud_upload</div>
<div class="mdl-tooltip mdl-tooltip--left" for="tt3">Upload</div>
<!-- Right-aligned tooltip -->
<div id="tt4" class="icon material-icons">cloud_download</div>
<div class="mdl-tooltip mdl-tooltip--right" for="tt4">Download</div>
<!-- Top-aligned tooltip -->
<div id="tt5" class="icon material-icons">favorite</div>
<div class="mdl-tooltip mdl-tooltip--top" for="tt5">Favorite</div>
<!-- Bottom-aligned tooltip -->
<div id="tt6" class="icon material-icons">delete</div>
<div class="mdl-tooltip mdl-tooltip--bottom" for="tt6">Delete</div>Usage Examples:
// Tooltips work automatically, but you can create them dynamically
function addTooltip(elementId, text, position = '') {
const element = document.getElementById(elementId);
if (!element) return;
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = `mdl-tooltip ${position}`;
tooltip.setAttribute('for', elementId);
tooltip.textContent = text;
// Insert tooltip after the target element
element.parentNode.insertBefore(tooltip, element.nextSibling);
// Upgrade tooltip
componentHandler.upgradeElement(tooltip);
}
// Dynamic tooltip management
function updateTooltip(elementId, newText) {
const tooltip = document.querySelector(`[for="${elementId}"]`);
if (tooltip) {
tooltip.textContent = newText;
}
}
// Conditional tooltips
function setupConditionalTooltips() {
document.addEventListener('mouseenter', (event) => {
if (event.target.matches('[data-dynamic-tooltip]')) {
const tooltipText = getTooltipText(event.target);
if (tooltipText) {
showDynamicTooltip(event.target, tooltipText);
}
}
});
}
function getTooltipText(element) {
// Return different tooltip text based on element state
if (element.disabled) {
return 'This action is currently disabled';
} else if (element.classList.contains('loading')) {
return 'Please wait...';
} else {
return element.dataset.dynamicTooltip;
}
}/**
* Material Snackbar constants
*/
interface SnackbarConstants {
/** Default animation length in milliseconds */
ANIMATION_LENGTH: 250;
/** Default timeout in milliseconds */
DEFAULT_TIMEOUT: 2750;
}
/**
* Material Progress constants
*/
interface ProgressConstants {
/** CSS class for indeterminate progress */
INDETERMINATE_CLASS: 'mdl-progress__indeterminate';
}// Queue system for managing multiple notifications
class NotificationManager {
constructor(snackbarElement) {
this.snackbar = snackbarElement.MaterialSnackbar;
this.queue = [];
this.isShowing = false;
}
show(data) {
this.queue.push(data);
this.processQueue();
}
processQueue() {
if (this.isShowing || this.queue.length === 0) {
return;
}
const nextNotification = this.queue.shift();
this.isShowing = true;
// Add completion callback
const originalHandler = nextNotification.actionHandler;
const timeout = nextNotification.timeout || 2750;
nextNotification.actionHandler = () => {
if (originalHandler) originalHandler();
this.onNotificationComplete();
};
this.snackbar.showSnackbar(nextNotification);
// Auto-complete after timeout
setTimeout(() => {
this.onNotificationComplete();
}, timeout + 500); // Add buffer for animation
}
onNotificationComplete() {
this.isShowing = false;
setTimeout(() => this.processQueue(), 300);
}
}
// Usage
const notificationManager = new NotificationManager(
document.querySelector('#notification-snackbar')
);
// Show notifications that will be queued
notificationManager.show({ message: 'First notification' });
notificationManager.show({ message: 'Second notification' });
notificationManager.show({ message: 'Third notification' });// Multi-step progress tracking
class ProgressTracker {
constructor(progressElement) {
this.progress = progressElement.MaterialProgress;
this.steps = [];
this.currentStep = 0;
}
addStep(name, weight = 1) {
this.steps.push({ name, weight, completed: false });
return this;
}
completeStep(stepIndex) {
if (stepIndex < this.steps.length) {
this.steps[stepIndex].completed = true;
this.updateProgress();
}
}
completeCurrentStep() {
this.completeStep(this.currentStep);
this.currentStep++;
}
updateProgress() {
const totalWeight = this.steps.reduce((sum, step) => sum + step.weight, 0);
const completedWeight = this.steps
.filter(step => step.completed)
.reduce((sum, step) => sum + step.weight, 0);
const percentage = (completedWeight / totalWeight) * 100;
this.progress.setProgress(percentage);
}
reset() {
this.steps.forEach(step => step.completed = false);
this.currentStep = 0;
this.progress.setProgress(0);
}
}
// Usage
const tracker = new ProgressTracker(document.querySelector('#multi-step-progress'))
.addStep('Initialize', 1)
.addStep('Load Data', 2)
.addStep('Process Data', 3)
.addStep('Save Results', 1);
async function performMultiStepOperation() {
tracker.reset();
// Step 1
await initialize();
tracker.completeCurrentStep();
// Step 2
await loadData();
tracker.completeCurrentStep();
// Step 3
await processData();
tracker.completeCurrentStep();
// Step 4
await saveResults();
tracker.completeCurrentStep();
}// Centralized loading state management
class LoadingStateManager {
constructor() {
this.loadingStates = new Map();
this.globalSpinner = document.querySelector('#global-spinner')?.MaterialSpinner;
}
setLoading(key, isLoading, options = {}) {
if (isLoading) {
this.loadingStates.set(key, options);
} else {
this.loadingStates.delete(key);
}
this.updateGlobalState();
this.updateSpecificElements(key, isLoading, options);
}
updateGlobalState() {
const hasAnyLoading = this.loadingStates.size > 0;
if (this.globalSpinner) {
if (hasAnyLoading) {
this.globalSpinner.start();
} else {
this.globalSpinner.stop();
}
}
// Update body class for global loading styles
document.body.classList.toggle('is-loading', hasAnyLoading);
}
updateSpecificElements(key, isLoading, options) {
const { spinner, progress, button } = options;
if (spinner) {
const spinnerElement = document.querySelector(spinner);
if (spinnerElement?.MaterialSpinner) {
if (isLoading) {
spinnerElement.MaterialSpinner.start();
} else {
spinnerElement.MaterialSpinner.stop();
}
}
}
if (button) {
const buttonElement = document.querySelector(button);
if (buttonElement) {
buttonElement.disabled = isLoading;
if (buttonElement.MaterialButton) {
if (isLoading) {
buttonElement.MaterialButton.disable();
} else {
buttonElement.MaterialButton.enable();
}
}
}
}
if (progress && typeof progress === 'object') {
const progressElement = document.querySelector(progress.selector);
if (progressElement?.MaterialProgress) {
if (!isLoading) {
progressElement.MaterialProgress.setProgress(100);
setTimeout(() => {
progressElement.MaterialProgress.setProgress(0);
}, 500);
}
}
}
}
isLoading(key) {
return this.loadingStates.has(key);
}
hasAnyLoading() {
return this.loadingStates.size > 0;
}
}
// Global loading manager
const loadingManager = new LoadingStateManager();
// Usage
async function saveUserData() {
loadingManager.setLoading('save-user', true, {
spinner: '#save-spinner',
button: '#save-button',
progress: { selector: '#save-progress' }
});
try {
await api.saveUser(userData);
notificationManager.show({ message: 'User saved successfully' });
} catch (error) {
notificationManager.show({
message: 'Failed to save user',
actionText: 'Retry',
actionHandler: () => saveUserData()
});
} finally {
loadingManager.setLoading('save-user', false);
}
}