UI Components Medium
Dashboard Widget
A draggable, resizable dashboard widget system. Widgets can be minimized, refreshed, and rearranged to customize the dashboard layout.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
padding: 1.5rem;
}
/* ── Dashboard header ── */
.dashboard-header {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.dashboard-title {
font-size: 1.25rem;
font-weight: 700;
color: #f1f5f9;
}
.dashboard-subtitle {
font-size: 0.8125rem;
color: #475569;
}
/* ── Widget Grid ── */
.widget-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
align-items: start;
}
/* ── Widget Card ── */
.widget {
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 12px;
overflow: hidden;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.widget.closing {
opacity: 0;
transform: scale(0.96);
pointer-events: none;
}
/* ── Widget Header ── */
.widget-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0.875rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
background: rgba(255, 255, 255, 0.02);
gap: 0.5rem;
}
.widget.minimized .widget-header {
border-bottom: none;
}
.widget-header-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.widget-icon {
width: 26px;
height: 26px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.widget-icon--green {
background: rgba(52, 211, 153, 0.12);
color: #34d399;
}
.widget-icon--blue {
background: rgba(56, 189, 248, 0.12);
color: #38bdf8;
}
.widget-icon--purple {
background: rgba(167, 139, 250, 0.12);
color: #a78bfa;
}
.widget-icon--cyan {
background: rgba(34, 211, 238, 0.12);
color: #22d3ee;
}
.widget-title {
font-size: 0.8125rem;
font-weight: 600;
color: #e2e8f0;
}
/* Controls */
.widget-controls {
display: flex;
align-items: center;
gap: 0.375rem;
}
.widget-timestamp {
font-size: 0.6875rem;
color: #475569;
white-space: nowrap;
}
.widget-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: #64748b;
border-radius: 5px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.widget-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #cbd5e1;
}
.widget-btn--close:hover {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
/* Refresh spin */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.widget-btn.refreshing .refresh-icon {
animation: spin 0.8s linear infinite;
}
/* ── Widget Body ── */
.widget-body {
padding: 1rem 0.875rem;
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.3s ease;
overflow: hidden;
}
.widget.minimized .widget-body {
grid-template-rows: 0fr;
padding-top: 0;
padding-bottom: 0;
}
.widget-body > * {
overflow: hidden;
}
/* ── Bar Chart ── */
.bar-chart {
display: flex;
gap: 0.625rem;
height: 140px;
}
.bar-labels {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2px 0;
text-align: right;
}
.bar-labels span {
font-size: 0.65rem;
color: #475569;
}
.bars {
flex: 1;
display: flex;
align-items: flex-end;
gap: 0.5rem;
}
.bar-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.375rem;
height: 100%;
justify-content: flex-end;
}
.bar {
position: relative;
width: 100%;
height: var(--h);
background: rgba(255, 255, 255, 0.08);
border-radius: 4px 4px 2px 2px;
transition: background 0.2s;
}
.bar:hover {
background: rgba(255, 255, 255, 0.14);
}
.bar--accent {
background: rgba(56, 189, 248, 0.5);
}
.bar--accent:hover {
background: rgba(56, 189, 248, 0.65);
}
.bar--live {
background: rgba(52, 211, 153, 0.35);
}
.bar--live:hover {
background: rgba(52, 211, 153, 0.5);
}
.bar-tip {
position: absolute;
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
background: #334155;
color: #f1f5f9;
font-size: 0.6rem;
padding: 2px 5px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
}
.bar:hover .bar-tip {
opacity: 1;
}
.bar-name {
font-size: 0.65rem;
color: #64748b;
}
/* ── User List ── */
.user-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.user-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.user-row:last-child {
border-bottom: none;
}
.user-av {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--c, #6366f1);
color: #fff;
font-size: 0.7rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
opacity: 0.9;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.user-name {
font-size: 0.8125rem;
font-weight: 500;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-email {
font-size: 0.7rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-badge {
font-size: 0.65rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 20px;
white-space: nowrap;
}
.status-badge--active {
background: rgba(52, 211, 153, 0.12);
color: #34d399;
}
.status-badge--pending {
background: rgba(251, 191, 36, 0.12);
color: #fbbf24;
}
.status-badge--inactive {
background: rgba(148, 163, 184, 0.1);
color: #94a3b8;
}
/* ── Task List ── */
.task-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 0.875rem;
}
.task-item {
padding: 0.45rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.task-item:last-child {
border-bottom: none;
}
.task-label {
display: flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
}
.task-label input[type="checkbox"] {
display: none;
}
.task-check {
width: 16px;
height: 16px;
border: 2px solid #334155;
border-radius: 4px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, border-color 0.15s;
position: relative;
}
.task-label input:checked + .task-check {
background: #38bdf8;
border-color: #38bdf8;
}
.task-label input:checked + .task-check::after {
content: "";
display: block;
width: 4px;
height: 7px;
border: 2px solid #0f172a;
border-top: none;
border-left: none;
transform: rotate(45deg) translate(-1px, -1px);
}
.task-text {
font-size: 0.8125rem;
color: #cbd5e1;
transition: color 0.15s, text-decoration 0.15s;
}
.task-label input:checked ~ .task-text {
color: #475569;
text-decoration: line-through;
}
.task-progress {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.task-progress-bar {
height: 4px;
background: rgba(255, 255, 255, 0.07);
border-radius: 2px;
overflow: hidden;
}
.task-progress-fill {
height: 100%;
background: #38bdf8;
border-radius: 2px;
transition: width 0.4s ease;
}
.task-progress-label {
font-size: 0.7rem;
color: #64748b;
}
/* ── Health Bars ── */
.health-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.health-item {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.health-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.health-label {
font-size: 0.8rem;
color: #94a3b8;
}
.health-val {
font-size: 0.8rem;
font-weight: 600;
}
.health-val--ok {
color: #34d399;
}
.health-val--warn {
color: #fbbf24;
}
.health-val--crit {
color: #f87171;
}
.health-bar {
height: 6px;
background: rgba(255, 255, 255, 0.07);
border-radius: 3px;
overflow: hidden;
}
.health-fill {
height: 100%;
border-radius: 3px;
transition: width 0.6s ease;
}
.health-fill--ok {
background: linear-gradient(90deg, #059669, #34d399);
}
.health-fill--warn {
background: linear-gradient(90deg, #d97706, #fbbf24);
}
.health-fill--crit {
background: linear-gradient(90deg, #dc2626, #f87171);
}
/* ── Restore Bar ── */
.restore-bar {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
transition: transform 0.3s cubic-bezier(0.34, 1.2, 0.64, 1);
z-index: 200;
white-space: nowrap;
}
.restore-bar.visible {
transform: translateX(-50%) translateY(0);
}
.restore-msg {
font-size: 0.875rem;
color: #94a3b8;
}
.restore-btn {
background: #38bdf8;
color: #0f172a;
border: none;
border-radius: 6px;
padding: 0.3rem 0.75rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.restore-btn:hover {
background: #7dd3fc;
}
/* ── Responsive ── */
@media (max-width: 640px) {
.widget-grid {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
.widget,
.widget-body,
.restore-bar {
transition: none !important;
}
.widget-btn.refreshing .refresh-icon {
animation: none;
}
}(() => {
const restoreBar = document.getElementById("restoreBar");
const restoreMsg = document.getElementById("restoreMsg");
const restoreBtn = document.getElementById("restoreBtn");
const closedWidgets = new Map(); // id -> { element, placeholder }
let hideRestoreTimer = null;
function getTimestamp() {
const now = new Date();
return now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function showRestoreBar(widgetTitle, widgetId) {
clearTimeout(hideRestoreTimer);
restoreMsg.textContent = `"${widgetTitle}" was closed`;
restoreBar.classList.add("visible");
restoreBtn.onclick = () => restoreWidget(widgetId);
hideRestoreTimer = setTimeout(() => {
restoreBar.classList.remove("visible");
}, 6000);
}
function restoreWidget(widgetId) {
const stored = closedWidgets.get(widgetId);
if (!stored) return;
const { element, placeholder } = stored;
element.style.opacity = "0";
element.style.transform = "scale(0.96)";
placeholder.replaceWith(element);
// trigger reflow then animate in
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.transition = "opacity 0.3s ease, transform 0.3s ease";
element.style.opacity = "1";
element.style.transform = "scale(1)";
setTimeout(() => {
element.style.transition = "";
element.style.opacity = "";
element.style.transform = "";
}, 350);
});
});
closedWidgets.delete(widgetId);
restoreBar.classList.remove("visible");
}
// ── Event delegation on widget grid ──
document.getElementById("widgetGrid").addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const widget = btn.closest(".widget");
if (!widget) return;
const action = btn.dataset.action;
if (action === "minimize") {
widget.classList.toggle("minimized");
// Update tooltip
btn.title = widget.classList.contains("minimized") ? "Maximize" : "Minimize";
// Swap icon: minus <-> plus
const svg = btn.querySelector("svg line");
if (svg) {
if (widget.classList.contains("minimized")) {
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`;
} else {
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/></svg>`;
}
}
}
if (action === "refresh") {
if (btn.classList.contains("refreshing")) return;
btn.classList.add("refreshing");
setTimeout(() => {
btn.classList.remove("refreshing");
const ts = widget.querySelector(".widget-timestamp");
if (ts) ts.textContent = `Updated at ${getTimestamp()}`;
}, 1000);
}
if (action === "close") {
const widgetId = widget.id;
const widgetTitle = widget.dataset.title || "Widget";
// Create invisible placeholder to preserve grid layout briefly
const placeholder = document.createElement("div");
placeholder.style.cssText = `width:${widget.offsetWidth}px; height:${widget.offsetHeight}px; visibility:hidden; pointer-events:none;`;
widget.classList.add("closing");
setTimeout(() => {
widget.replaceWith(placeholder);
closedWidgets.set(widgetId, { element: widget, placeholder });
showRestoreBar(widgetTitle, widgetId);
// Clean placeholder after animation
setTimeout(() => {
if (placeholder.parentNode) placeholder.remove();
}, 400);
}, 320);
}
});
// Also handle task checkbox interactions to update progress
document.querySelectorAll(".task-list").forEach((list) => {
list.addEventListener("change", (e) => {
if (!e.target.matches('input[type="checkbox"]')) return;
const all = list.querySelectorAll('input[type="checkbox"]');
const checked = list.querySelectorAll('input[type="checkbox"]:checked');
const widget = list.closest(".widget");
const fill = widget.querySelector(".task-progress-fill");
const label = widget.querySelector(".task-progress-label");
if (fill) fill.style.width = `${(checked.length / all.length) * 100}%`;
if (label) label.textContent = `${checked.length} of ${all.length} complete`;
});
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard Widget</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Restore notification -->
<div class="restore-bar" id="restoreBar">
<span class="restore-msg" id="restoreMsg"></span>
<button class="restore-btn" id="restoreBtn">Restore</button>
</div>
<div class="dashboard-header">
<h1 class="dashboard-title">My Dashboard</h1>
<span class="dashboard-subtitle">March 2026</span>
</div>
<div class="widget-grid" id="widgetGrid">
<!-- Widget 1: Revenue Chart -->
<div class="widget" id="w-revenue" data-title="Revenue Chart">
<div class="widget-header">
<div class="widget-header-left">
<div class="widget-icon widget-icon--green">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/><line x1="2" y1="20" x2="22" y2="20"/>
</svg>
</div>
<span class="widget-title">Revenue Chart</span>
</div>
<div class="widget-controls">
<span class="widget-timestamp">Updated just now</span>
<button class="widget-btn" data-action="refresh" title="Refresh">
<svg class="refresh-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
<button class="widget-btn" data-action="minimize" title="Minimize">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
<button class="widget-btn widget-btn--close" data-action="close" title="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
<div class="widget-body">
<div class="bar-chart">
<div class="bar-labels">
<span>$12k</span><span>$9k</span><span>$6k</span><span>$3k</span><span>$0</span>
</div>
<div class="bars">
<div class="bar-col"><div class="bar" style="--h:65%"><span class="bar-tip">$7.8k</span></div><span class="bar-name">Oct</span></div>
<div class="bar-col"><div class="bar" style="--h:82%"><span class="bar-tip">$9.8k</span></div><span class="bar-name">Nov</span></div>
<div class="bar-col"><div class="bar bar--accent" style="--h:100%"><span class="bar-tip">$12k</span></div><span class="bar-name">Dec</span></div>
<div class="bar-col"><div class="bar" style="--h:54%"><span class="bar-tip">$6.5k</span></div><span class="bar-name">Jan</span></div>
<div class="bar-col"><div class="bar" style="--h:71%"><span class="bar-tip">$8.5k</span></div><span class="bar-name">Feb</span></div>
<div class="bar-col"><div class="bar bar--live" style="--h:48%"><span class="bar-tip">$5.8k</span></div><span class="bar-name">Mar</span></div>
</div>
</div>
</div>
</div>
<!-- Widget 2: Recent Users -->
<div class="widget" id="w-users" data-title="Recent Users">
<div class="widget-header">
<div class="widget-header-left">
<div class="widget-icon widget-icon--blue">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<span class="widget-title">Recent Users</span>
</div>
<div class="widget-controls">
<span class="widget-timestamp">Updated just now</span>
<button class="widget-btn" data-action="refresh" title="Refresh">
<svg class="refresh-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
<button class="widget-btn" data-action="minimize" title="Minimize">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
<button class="widget-btn widget-btn--close" data-action="close" title="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
<div class="widget-body">
<ul class="user-list">
<li class="user-row">
<div class="user-av" style="--c:#6366f1">SK</div>
<div class="user-info"><span class="user-name">Sarah Kim</span><span class="user-email">sarah@acme.io</span></div>
<span class="status-badge status-badge--active">Active</span>
</li>
<li class="user-row">
<div class="user-av" style="--c:#0ea5e9">MR</div>
<div class="user-info"><span class="user-name">Marcus Reed</span><span class="user-email">m.reed@corp.com</span></div>
<span class="status-badge status-badge--active">Active</span>
</li>
<li class="user-row">
<div class="user-av" style="--c:#f59e0b">JL</div>
<div class="user-info"><span class="user-name">Julia Lee</span><span class="user-email">julia@startup.dev</span></div>
<span class="status-badge status-badge--pending">Pending</span>
</li>
<li class="user-row">
<div class="user-av" style="--c:#10b981">TN</div>
<div class="user-info"><span class="user-name">Tom Nakamura</span><span class="user-email">t.nakamura@labs.ai</span></div>
<span class="status-badge status-badge--inactive">Inactive</span>
</li>
</ul>
</div>
</div>
<!-- Widget 3: Quick Tasks -->
<div class="widget" id="w-tasks" data-title="Quick Tasks">
<div class="widget-header">
<div class="widget-header-left">
<div class="widget-icon widget-icon--purple">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="9 11 12 14 22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
</div>
<span class="widget-title">Quick Tasks</span>
</div>
<div class="widget-controls">
<span class="widget-timestamp">Updated just now</span>
<button class="widget-btn" data-action="refresh" title="Refresh">
<svg class="refresh-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
<button class="widget-btn" data-action="minimize" title="Minimize">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
<button class="widget-btn widget-btn--close" data-action="close" title="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
<div class="widget-body">
<ul class="task-list">
<li class="task-item">
<label class="task-label">
<input type="checkbox" checked />
<span class="task-check"></span>
<span class="task-text">Review Q1 analytics report</span>
</label>
</li>
<li class="task-item">
<label class="task-label">
<input type="checkbox" />
<span class="task-check"></span>
<span class="task-text">Onboard new team members</span>
</label>
</li>
<li class="task-item">
<label class="task-label">
<input type="checkbox" />
<span class="task-check"></span>
<span class="task-text">Deploy v2.4 to production</span>
</label>
</li>
<li class="task-item">
<label class="task-label">
<input type="checkbox" checked />
<span class="task-check"></span>
<span class="task-text">Send invoice to Acme Corp</span>
</label>
</li>
</ul>
<div class="task-progress">
<div class="task-progress-bar">
<div class="task-progress-fill" style="width: 50%"></div>
</div>
<span class="task-progress-label">2 of 4 complete</span>
</div>
</div>
</div>
<!-- Widget 4: System Health -->
<div class="widget" id="w-system" data-title="System Health">
<div class="widget-header">
<div class="widget-header-left">
<div class="widget-icon widget-icon--cyan">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<span class="widget-title">System Health</span>
</div>
<div class="widget-controls">
<span class="widget-timestamp">Updated just now</span>
<button class="widget-btn" data-action="refresh" title="Refresh">
<svg class="refresh-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
</button>
<button class="widget-btn" data-action="minimize" title="Minimize">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
<button class="widget-btn widget-btn--close" data-action="close" title="Close">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
<div class="widget-body">
<ul class="health-list">
<li class="health-item">
<div class="health-meta">
<span class="health-label">CPU Usage</span>
<span class="health-val health-val--warn">72%</span>
</div>
<div class="health-bar"><div class="health-fill health-fill--warn" style="width:72%"></div></div>
</li>
<li class="health-item">
<div class="health-meta">
<span class="health-label">Memory</span>
<span class="health-val health-val--ok">48%</span>
</div>
<div class="health-bar"><div class="health-fill health-fill--ok" style="width:48%"></div></div>
</li>
<li class="health-item">
<div class="health-meta">
<span class="health-label">Disk</span>
<span class="health-val health-val--ok">31%</span>
</div>
<div class="health-bar"><div class="health-fill health-fill--ok" style="width:31%"></div></div>
</li>
<li class="health-item">
<div class="health-meta">
<span class="health-label">Network</span>
<span class="health-val health-val--crit">89%</span>
</div>
<div class="health-bar"><div class="health-fill health-fill--crit" style="width:89%"></div></div>
</li>
</ul>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Dashboard Widget
An interactive dashboard widget system with four pre-built widget types: a bar chart, user list, task checklist, and system health monitor. Each widget supports minimize/maximize, refresh with animated spinner, and close with a restore toast.
Features
- Minimize collapses widget body to show only the header bar
- Refresh button spins for 1 second then updates a “Last updated” timestamp
- Close removes the widget with a fade-out animation; a restore notification appears
- Fully dark-themed with glassy card styling and smooth CSS transitions