A production-ready component library for Roovix Research Lab. Dark-first, accessible, and built for cloud, developer, and social interfaces.
Colors
Semantic tokens that adapt to dark and light themes.
--color-canvas-default
--color-canvas-inset
--color-canvas-subtle
--color-canvas-overlay
--color-canvas-elevatedDefault — Primary text
Muted — Secondary descriptions
Subtle — Hints, captions, placeholders
--color-fg-default
--color-fg-muted
--color-fg-subtle--color-accent-fg
--color-accent-emphasis
--color-accent-muted
--color-accent-subtle
--color-accent-hover
/* same for success, warning, danger, info */Typography
Clean hierarchy with system + Inter fallback.
<h1 style="font-size:clamp(1.8rem,4vw,2.5rem);font-weight:700;">Heading 1</h1>
<p style="font-size:1rem;">Body text</p>
<p style="font-size:0.875rem;color:var(--color-fg-muted);">Small</p>Spacing & Radius
Consistent spacing scale and corner radii.
--space-1: 4px; --space-2: 8px; --space-3: 12px;
--space-4: 16px; --space-5: 24px; --space-6: 32px;--radius-1: 4px; --radius-2: 8px; --radius-3: 12px;
--radius-4: 16px; --radius-full: 9999px;Shadows & Borders
Elevation and border tokens.
--shadow-xs
--shadow-sm
--shadow-md
--shadow-lg
--shadow-xl--color-border-subtle
--color-border-muted
--color-border-default
--color-border-hard
--color-border-accentMotion
Subtle transitions and animations.
--transition-fast: 0.10s ease;
--transition-base: 0.18s ease;
--transition-slow: 0.30s ease;Form Elements
Inputs, selects, checkboxes, radios, switches, and ranges.
<input class="input" type="text" placeholder="Text input" />
<input class="input error" type="text" placeholder="Error" />
<textarea class="input textarea" rows="3" placeholder="Textarea"></textarea><div class="input-group">
<span class="input-group-addon">https://</span>
<input class="input" type="text" />
</div><!-- Custom Select -->
<div class="select-wrap custom-select">
<button class="select-trigger" type="button">
<span>Option one</span>
<span class="select-arrow"></span>
</button>
<div class="select-menu">
<div class="select-option selected">Option one</div>
<div class="select-option">Option two</div>
<div class="select-option">Option three</div>
</div>
</div>
<!-- JavaScript -->
<script>
document.querySelectorAll(".custom-select").forEach((select) => {
const trigger = select.querySelector(".select-trigger");
const options = select.querySelectorAll(".select-option");
trigger.addEventListener("click", (event) => {
event.stopPropagation();
document.querySelectorAll(".custom-select.open")
.forEach((other) => {
if (other !== select) {
other.classList.remove("open");
}
});
select.classList.toggle("open");
});
options.forEach((option) => {
option.addEventListener("click", () => {
options.forEach((item) =>
item.classList.remove("selected")
);
option.classList.add("selected");
trigger.querySelector("span").textContent =
option.textContent;
select.classList.remove("open");
});
});
});
document.addEventListener("click", () => {
document.querySelectorAll(".custom-select.open")
.forEach((select) => {
select.classList.remove("open");
});
});
</script>
<label class="control"><input type="checkbox" /> Label</label>
<label class="switch"><input type="checkbox" /><span class="slider"></span></label><input class="range" type="range" min="0" max="100" value="60" />Alerts & Toasts
Inline alerts and toast notifications.
<div class="alert alert-success">…</div>
<div class="alert alert-danger">…</div><div class="toast toast-success">…</div>Badges & Tags
Status indicators and metadata labels.
<span class="badge badge-accent">Active</span>
<span class="badge badge-success">Online</span><span class="tag">javascript</span>
<span class="tag tag-accent">react</span>Loading
Spinners, skeletons, and progress indicators.
<span class="spinner"></span>
<span class="spinner spinner-sm"></span>
<span class="spinner spinner-lg"></span>
<div class="spinner-dots"><span></span><span></span><span></span></div><div class="skeleton skeleton-text" style="width:70%;"></div>
<div class="skeleton skeleton-avatar"></div>
<div class="skeleton skeleton-card">…</div><div class="progress"><div class="bar" style="width:45%;"></div></div>
<div class="progress indeterminate"><div class="bar"></div></div>Tabs
Horizontal tab navigation.
<div class="tabs" role="tablist">
<button class="tab active" role="tab">Overview</button>
<button class="tab" role="tab">Metrics</button>
</div>
<!-- JavaScript -->
<script>
// ---------- Tabs interaction ----------
document.querySelectorAll('.tabs').forEach(function(tabGroup) {
const tabs = tabGroup.querySelectorAll('.tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
tabs.forEach(function(t) { t.classList.remove('active'); });
this.classList.add('active');
const body = this.closest('.demo-body');
if (body) {
const desc = body.querySelector('div:last-child');
if (desc && desc.style && desc.textContent.includes('Active tab:')) {
desc.textContent = 'Active tab: ' + this.textContent;
}
}
});
});
});
</script>
Pagination
Page navigation controls.
<nav class="pagination" aria-label="Pagination">
<button class="page" disabled>‹</button>
<button class="page active">1</button>
<button class="page">2</button>
<button class="page">›</button>
</nav>Cards & Stats
Content containers and metric cards.
Roovix Compute
Running4 vCPUs · 16 GB RAM · Ubuntu 22.04
Roovix Storage
Syncing256 GB SSD · 78% used
Roovix API
Stablev3.2.1 · 99.97% uptime
<div class="card">…</div><div class="card card-stat">
<div class="stat-value">1,284</div>
<div class="stat-label">Total requests</div>
</div>Tables
Data tables with clean styling.
| Instance | Status | CPU | Memory | Region |
|---|---|---|---|---|
| compute-01 | Running | 42% | 6.2/16 GB | us-west-2 |
| compute-02 | Stopping | 12% | 2.1/16 GB | us-east-1 |
| compute-03 | Failed | 0% | 0/16 GB | eu-central-1 |
| compute-04 | Provisioning | — | — | ap-south-1 |
<div class="table-wrap">
<table>…</table>
</div>Avatars
User avatars and groups.
<div class="avatar">A</div>
<div class="avatar avatar-lg">B</div>
<div class="avatar avatar-sm">C</div><div class="avatar-group">
<div class="avatar">A</div>
<div class="avatar">B</div>
</div>Modal
Dialog overlays for focus actions.
<div class="modal-overlay">
<div class="modal">
<button class="modal-close">✕</button>
<div class="modal-title">…</div>
<div class="modal-body">…</div>
<div class="modal-footer">…</div>
</div>
</div>Tooltip
Hover-triggered context tips.
<div class="tooltip-trigger">
<button class="btn">Hover me</button>
<span class="tooltip">Tooltip text</span>
</div>Dropdown
Contextual menus.
<div class="dropdown">
<button class="btn btn-secondary">Actions ▾</button>
<div class="dropdown-menu">
<div class="dropdown-header">Manage</div>
<button class="dropdown-item">Edit</button>
<button class="dropdown-item">Delete</button>
</div>
</div>
<!-- JavaScript -->
<script>
// ---------- Dropdown ----------
const dropdownToggle = document.getElementById('dropdownToggle');
const dropdownMenu = document.getElementById('dropdownMenu');
if (dropdownToggle && dropdownMenu) {
dropdownToggle.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = dropdownMenu.classList.contains('open');
dropdownMenu.classList.toggle('open', !isOpen);
});
document.addEventListener('click', function() {
dropdownMenu.classList.remove('open');
});
// Close on escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && dropdownMenu.classList.contains('open')) {
dropdownMenu.classList.remove('open');
}
});
// Prevent closing when clicking inside menu
dropdownMenu.addEventListener('click', function(e) {
e.stopPropagation();
});
// Item clicks
dropdownMenu.querySelectorAll('.dropdown-item').forEach(function(item) {
item.addEventListener('click', function() {
// Just close for demo
dropdownMenu.classList.remove('open');
});
});
}
</script>
File Upload
Drop zones, file items, upload progress, and real drag-and-drop management.
<!-- HTML -->
<div class="drop-zone" id="dropZone">
<div class="dz-icon">⬇</div>
<div class="dz-text">Drop files here or click to browse</div>
<div class="dz-hint">Supports images, documents, archives — up to 50MB per file</div>
<input type="file" id="fileInput" multiple style="display: none;" />
</div>
<button class="btn btn-primary btn-sm" id="uploadAllBtn">⬆ Upload All</button>
<button class="btn btn-secondary btn-sm" id="clearAllBtn">✕ Clear All</button>
<div id="fileList"></div>
<!-- JavaScript -->
<script>
(function() {
'use strict';
// ---------- DOM refs ----------
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const emptyMessage = document.getElementById('emptyMessage');
const fileCount = document.getElementById('fileCount');
const uploadAllBtn = document.getElementById('uploadAllBtn');
const clearAllBtn = document.getElementById('clearAllBtn');
const uploadStatus = document.getElementById('uploadStatus');
// ---------- State ----------
let files = []; // Array of { file: File, id: string, status: 'idle'|'uploading'|'done'|'error', progress: 0 }
// ---------- Helpers ----------
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const val = (bytes / Math.pow(k, i)).toFixed(1);
return val + ' ' + sizes[i];
}
function getFileIcon(file) {
const type = file.type;
if (type.startsWith('image/')) return '🖼';
if (type.startsWith('video/')) return '🎬';
if (type.startsWith('audio/')) return '🎵';
if (type.includes('pdf')) return '📄';
if (type.includes('zip') || type.includes('tar') || type.includes('gzip')) return '📦';
if (type.includes('javascript') || type.includes('json') || type.includes('yaml') || type.includes('xml')) return '📝';
return '📎';
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 6);
}
// ---------- Render file list ----------
function renderFiles() {
// Clear list
fileList.innerHTML = '';
if (files.length === 0) {
const msg = document.createElement('div');
msg.id = 'emptyMessage';
msg.style.cssText =
'text-align:center;color:var(--color-fg-subtle);font-size:0.85rem;padding:var(--space-3);';
msg.textContent = 'No files added yet. Drag & drop or click the zone above.';
fileList.appendChild(msg);
fileCount.textContent = '0 files';
return;
}
files.forEach(function(f) {
const item = document.createElement('div');
item.className = 'file-item';
item.dataset.id = f.id;
const icon = document.createElement('span');
icon.className = 'file-icon';
icon.textContent = getFileIcon(f.file);
const info = document.createElement('div');
info.className = 'file-info';
const name = document.createElement('div');
name.className = 'file-name';
name.textContent = f.file.name;
const size = document.createElement('div');
size.className = 'file-size';
size.textContent = formatSize(f.file.size);
info.appendChild(name);
info.appendChild(size);
// Progress bar if uploading
if (f.status === 'uploading') {
const progWrap = document.createElement('div');
progWrap.className = 'progress';
progWrap.style.cssText = 'width:100%;height:3px;margin-top:4px;';
const bar = document.createElement('div');
bar.className = 'bar';
bar.style.width = f.progress + '%';
progWrap.appendChild(bar);
info.appendChild(progWrap);
}
const statusEl = document.createElement('span');
statusEl.className = 'file-status';
if (f.status === 'idle') {
statusEl.textContent = '⏳ Pending';
statusEl.style.color = 'var(--color-fg-muted)';
} else if (f.status === 'uploading') {
statusEl.textContent = '⟳ ' + Math.round(f.progress) + '%';
statusEl.className = 'file-status uploading';
} else if (f.status === 'done') {
statusEl.textContent = '✓ Done';
statusEl.className = 'file-status success';
} else if (f.status === 'error') {
statusEl.textContent = '✕ Failed';
statusEl.className = 'file-status error';
}
// Remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-ghost btn-sm';
removeBtn.style.cssText = 'padding:0.1rem 0.4rem;font-size:0.7rem;color:var(--color-fg-subtle);';
removeBtn.textContent = '✕';
removeBtn.title = 'Remove file';
removeBtn.addEventListener('click', function(e) {
e.stopPropagation();
removeFile(f.id);
});
removeBtn.addEventListener('mouseenter', function() {
this.style.color = 'var(--color-danger-fg)';
});
removeBtn.addEventListener('mouseleave', function() {
this.style.color = 'var(--color-fg-subtle)';
});
item.appendChild(icon);
item.appendChild(info);
item.appendChild(statusEl);
item.appendChild(removeBtn);
fileList.appendChild(item);
});
fileCount.textContent = files.length + ' file' + (files.length > 1 ? 's' : '');
}
// ---------- Add files ----------
function addFiles(fileListFromInput) {
let added = 0;
for (let i = 0; i < fileListFromInput.length; i++) {
const file = fileListFromInput[i];
// Skip duplicates by name + size
const exists = files.some(function(f) {
return f.file.name === file.name && f.file.size === file.size;
});
if (!exists) {
files.push({
file: file,
id: generateId(),
status: 'idle',
progress: 0
});
added++;
}
}
if (added > 0) {
renderFiles();
// Show status briefly
uploadStatus.style.display = 'block';
uploadStatus.textContent = '✅ Added ' + added + ' file' + (added > 1 ? 's' : '');
uploadStatus.style.background = 'var(--color-success-subtle)';
uploadStatus.style.borderColor = 'var(--color-success-muted)';
uploadStatus.style.color = 'var(--color-success-fg)';
setTimeout(function() {
uploadStatus.style.display = 'none';
}, 2000);
} else {
// Duplicate
uploadStatus.style.display = 'block';
uploadStatus.textContent = '⚠️ All files already added';
uploadStatus.style.background = 'var(--color-warning-subtle)';
uploadStatus.style.borderColor = 'var(--color-warning-muted)';
uploadStatus.style.color = 'var(--color-warning-fg)';
setTimeout(function() {
uploadStatus.style.display = 'none';
}, 2000);
}
// Reset input
fileInput.value = '';
}
// ---------- Remove file ----------
function removeFile(id) {
files = files.filter(function(f) { return f.id !== id; });
renderFiles();
}
// ---------- Clear all ----------
function clearAll() {
if (files.length === 0) return;
const anyUploading = files.some(function(f) { return f.status === 'uploading'; });
if (anyUploading) {
if (!confirm('Some files are uploading. Clear anyway?')) return;
}
files = [];
renderFiles();
uploadStatus.style.display = 'block';
uploadStatus.textContent = '🗑️ All files cleared';
uploadStatus.style.background = 'var(--color-accent-subtle)';
uploadStatus.style.borderColor = 'var(--color-accent-muted)';
uploadStatus.style.color = 'var(--color-accent-fg)';
setTimeout(function() {
uploadStatus.style.display = 'none';
}, 1500);
}
// ---------- Simulate upload ----------
function simulateUpload(fileId) {
const f = files.find(function(item) { return item.id === fileId; });
if (!f) return;
f.status = 'uploading';
f.progress = 0;
renderFiles();
let progress = 0;
const interval = setInterval(function() {
progress += Math.random() * 12 + 2;
if (progress >= 100) {
progress = 100;
f.progress = 100;
f.status = 'done';
renderFiles();
clearInterval(interval);
// Update status toast
uploadStatus.style.display = 'block';
uploadStatus.textContent = '✅ ' + f.file.name + ' uploaded successfully';
uploadStatus.style.background = 'var(--color-success-subtle)';
uploadStatus.style.borderColor = 'var(--color-success-muted)';
uploadStatus.style.color = 'var(--color-success-fg)';
setTimeout(function() {
uploadStatus.style.display = 'none';
}, 2500);
return;
}
f.progress = Math.min(progress, 99);
renderFiles();
}, 150 + Math.random() * 200);
}
// ---------- Upload all ----------
function uploadAll() {
const idleFiles = files.filter(function(f) { return f.status === 'idle'; });
if (idleFiles.length === 0) {
uploadStatus.style.display = 'block';
uploadStatus.textContent = 'ℹ️ No pending files to upload';
uploadStatus.style.background = 'var(--color-accent-subtle)';
uploadStatus.style.borderColor = 'var(--color-accent-muted)';
uploadStatus.style.color = 'var(--color-accent-fg)';
setTimeout(function() {
uploadStatus.style.display = 'none';
}, 2000);
return;
}
uploadStatus.style.display = 'block';
uploadStatus.textContent = '⬆ Uploading ' + idleFiles.length + ' file' + (idleFiles.length > 1 ? 's' : '') +
'…';
uploadStatus.style.background = 'var(--color-accent-subtle)';
uploadStatus.style.borderColor = 'var(--color-accent-muted)';
uploadStatus.style.color = 'var(--color-accent-fg)';
idleFiles.forEach(function(f, index) {
// Stagger uploads
setTimeout(function() {
simulateUpload(f.id);
}, index * 400);
});
// Update status after all started
setTimeout(function() {
uploadStatus.textContent = '⬆ Uploading ' + idleFiles.length + ' files…';
}, 300);
}
// ---------- Event listeners ----------
// Click drop zone → open file picker
dropZone.addEventListener('click', function(e) {
if (e.target.tagName !== 'INPUT') {
fileInput.click();
}
});
// File input change
fileInput.addEventListener('change', function() {
if (this.files.length > 0) {
addFiles(this.files);
}
});
// Drag and drop events
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
dropZone.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
const dt = e.dataTransfer;
if (dt.files && dt.files.length > 0) {
addFiles(dt.files);
}
});
// Upload all button
uploadAllBtn.addEventListener('click', uploadAll);
// Clear all button
clearAllBtn.addEventListener('click', clearAll);
// ---------- Expose for copy-code snippets ----------
window.fileUploadDemo = {
addFiles: addFiles,
removeFile: removeFile,
clearAll: clearAll,
uploadAll: uploadAll,
files: files,
renderFiles: renderFiles
};
// ---------- Initial render ----------
renderFiles();
// Log to console for debugging
console.log('✅ File upload demo ready — use window.fileUploadDemo to control it.');
console.log('📂 Supported: drag & drop, multiple files, progress simulation.');
})();
</script>
<!-- File item — success -->
<div class="file-item">
<span class="file-icon">📄</span>
<div class="file-info">
<div class="file-name">deployment-spec.yaml</div>
<div class="file-size">2.4 KB</div>
</div>
<span class="file-status success">✓ Done</span>
</div>
<!-- File item — uploading with progress -->
<div class="file-item">
<span class="file-icon">🖼</span>
<div class="file-info">
<div class="file-name">image.png</div>
<div class="file-size">1.8 MB</div>
<div class="progress" style="width: 100%; height: 3px; margin-top: 4px;">
<div class="bar" style="width: 64%;"></div>
</div>
</div>
<span class="file-status uploading">⟳ 64%</span>
</div>
<!-- File item — error -->
<div class="file-item">
<span class="file-icon">📦</span>
<div class="file-info">
<div class="file-name">archive.tar.gz</div>
<div class="file-size">12.6 MB</div>
</div>
<span class="file-status error">✕ Failed</span>
</div>
Post & Comment
Social feed components.
<div class="card">
<div style="display:flex;gap:var(--space-3);">…</div>
<div>Post content…</div>
<div style="display:flex;gap:var(--space-4);">…</div>
</div><div style="display:flex;gap:var(--space-3);padding:var(--space-3);background:var(--color-canvas-inset);border:1px solid var(--color-border-subtle);border-radius:var(--radius-2);">…</div>Profile
User profile header and card.
<div style="display:flex;gap:var(--space-4);align-items:center;padding:var(--space-4);border:1px solid var(--color-border-subtle);border-radius:var(--radius-3);">…</div>