Styles #
CSS styling system for vlist with design tokens, variants, dark mode support, and performance-optimized CSS.
Overview #
vlist uses a CSS custom properties (design tokens) system that provides:
- Zero runtime overhead - Pure CSS, no JavaScript styling
- Dark mode support - Automatic via
prefers-color-schemeor manual via.darkclass - Customizable - Override tokens to match your design system
- Variants - Pre-built compact, comfortable, borderless, and striped styles
- Tailwind compatible - Works alongside Tailwind CSS v4+
- Performance-optimized - CSS containment,
will-change, and scroll transition suppression - Split CSS - Core styles (6.7 KB) separated from optional extras (3.4 KB)
Quick Start #
Import Styles #
import { vlist } from 'vlist';
import 'vlist/styles';
const list = vlist({
container: '#app',
item: {
height: 48,
template: (item) => `<div>${item.name}</div>`,
},
items: data
});
Import Optional Extras #
The CSS is split into core and extras for minimal bundle size:
| File | Size | Contents |
|---|---|---|
dist/vlist.css |
6.7 KB | Tokens, base layout, item states, custom scrollbar |
dist/vlist-extras.css |
3.4 KB | Variants, loading/empty states, utilities, animations |
// Core styles only (recommended minimum)
import 'vlist/styles';
// Optional extras (variants, loading states, animations)
import 'vlist/styles/extras';
Using a CDN #
<!-- Core styles -->
<link rel="stylesheet" href="https://unpkg.com/vlist/dist/vlist.css">
<!-- Optional extras -->
<link rel="stylesheet" href="https://unpkg.com/vlist/dist/vlist-extras.css">
CSS Classes #
Structure Classes #
| Class | Element | Description |
|---|---|---|
.vlist |
Root | Container element, sets dimensions and overflow |
.vlist-viewport |
Scrollable area | Handles scroll with native scrollbar |
.vlist-content |
Content wrapper | Sets total height (or width in horizontal mode) for scroll |
.vlist-items |
Items container | Holds rendered item elements |
.vlist-item |
Individual item | Positioned absolutely with transforms |
State Classes #
| Class | Description |
|---|---|
.vlist-item--selected |
Applied to selected items |
.vlist-item--focused |
Applied to keyboard-focused item |
.vlist-item--enter |
Applied briefly for fade-in animation |
.vlist--scrolling |
Applied to root during active scroll (suppresses CSS transitions) |
Layout Modifier Classes #
| Class | Description |
|---|---|
.vlist--horizontal |
Applied to root when orientation: 'horizontal'. Swaps scroll axis, item positioning, and border direction. Sets aria-orientation="horizontal". |
.vlist--grid |
Applied to root when layout: 'grid' |
.vlist--grouped |
Applied to root when groups config is present |
Custom Scrollbar Classes #
Used in compressed mode (1M+ items):
| Class | Description |
|---|---|
.vlist-scrollbar |
Scrollbar track container |
.vlist-scrollbar--visible |
Shows the scrollbar |
.vlist-scrollbar--dragging |
Active during thumb drag |
.vlist-scrollbar-thumb |
Draggable thumb element |
CSS Performance Optimizations #
vlist applies several CSS-level performance optimizations automatically:
CSS Containment #
The items container and individual items use CSS containment to help the browser optimize layout and paint:
/* Items container - layout and style containment */
.vlist-items {
contain: layout style;
}
/* Individual items - content containment + compositing hint */
.vlist-item {
contain: content;
will-change: transform;
}
contain: layout styleon the items container tells the browser that layout/style changes inside won't affect outside elementscontain: contenton items is a stricter containment that enables more aggressive optimizationwill-change: transformpromotes items to their own compositing layer for smooth GPU-accelerated positioning
Static Positioning via CSS #
Item positioning uses CSS classes instead of inline style.cssText. The .vlist-item class sets position: absolute; top: 0; left: 0; right: 0 — only the dynamic height and transform (for Y positioning) are set via JavaScript. This eliminates per-element CSS string parsing.
Scroll Transition Suppression #
During active scrolling, the .vlist--scrolling class is added to the root element. This disables CSS transitions on items to prevent visual jank:
/* Transitions are suppressed during scroll */
.vlist--scrolling .vlist-item {
transition: none !important;
}
When scrolling stops (idle detected), the class is removed and transitions are re-enabled. This ensures smooth 60fps scrolling while preserving animations during interaction.
Design Tokens #
All visual properties are controlled via CSS custom properties.
Colors (Light Mode) #
:root {
--vlist-bg: #ffffff; /* Background */
--vlist-bg-hover: #f9fafb; /* Hover state */
--vlist-bg-selected: #eff6ff; /* Selected state */
--vlist-bg-selected-hover: #dbeafe; /* Selected + hover */
--vlist-border: #e5e7eb; /* Borders */
--vlist-border-selected: #3b82f6; /* Selected indicator */
--vlist-text: #111827; /* Primary text */
--vlist-text-muted: #6b7280; /* Secondary text */
--vlist-focus-ring: #3b82f6; /* Focus outline */
--vlist-scrollbar-thumb: #d1d5db; /* Scrollbar thumb */
--vlist-scrollbar-thumb-hover: #9ca3af; /* Scrollbar hover */
}
Colors (Dark Mode) #
Automatically applied via prefers-color-scheme: dark or .dark class:
:root {
--vlist-bg: #111827;
--vlist-bg-hover: #1f2937;
--vlist-bg-selected: rgba(59, 130, 246, 0.2);
--vlist-bg-selected-hover: rgba(59, 130, 246, 0.3);
--vlist-border: #374151;
--vlist-border-selected: #3b82f6;
--vlist-text: #f9fafb;
--vlist-text-muted: #9ca3af;
--vlist-scrollbar-thumb: #4b5563;
--vlist-scrollbar-thumb-hover: #6b7280;
}
Spacing #
:root {
--vlist-item-padding-x: 1rem; /* Horizontal padding */
--vlist-item-padding-y: 0.75rem; /* Vertical padding */
--vlist-border-radius: 0.5rem; /* Container radius */
}
Custom Scrollbar (Compressed Mode) #
:root {
--vlist-scrollbar-width: 8px;
--vlist-scrollbar-track-bg: transparent;
--vlist-scrollbar-custom-thumb-bg: rgba(0, 0, 0, 0.3);
--vlist-scrollbar-custom-thumb-hover-bg: rgba(0, 0, 0, 0.5);
--vlist-scrollbar-custom-thumb-radius: 4px;
}
Transitions #
:root {
--vlist-transition-duration: 150ms;
--vlist-transition-timing: ease-in-out;
}
Customization #
Override Tokens #
Apply custom values to match your design system:
/* Custom theme */
:root {
--vlist-bg: #fafafa;
--vlist-bg-selected: #e0f2fe;
--vlist-border-selected: #0ea5e9;
--vlist-focus-ring: #0ea5e9;
--vlist-border-radius: 0.75rem;
}
Scoped Customization #
Apply different styles to specific lists:
.my-custom-list {
--vlist-bg: #1e1e2e;
--vlist-text: #cdd6f4;
--vlist-bg-selected: #45475a;
}
<div id="my-list" class="my-custom-list"></div>
Custom Class Prefix #
Change the default vlist prefix:
const list = vlist({
container: '#app',
classPrefix: 'mylist', // Uses .mylist, .mylist-item, etc.
// ...
});
Then update your CSS:
.mylist { /* ... */ }
.mylist-item { /* ... */ }
.mylist-item--selected { /* ... */ }
Variants #
Compact #
Reduced padding for dense lists:
<div id="list" class="vlist vlist--compact"></div>
.vlist--compact .vlist-item {
padding: 0.5rem 0.75rem;
}
Comfortable #
Increased padding for touch-friendly lists:
<div id="list" class="vlist vlist--comfortable"></div>
.vlist--comfortable .vlist-item {
padding: 1rem 1.25rem;
}
Borderless #
Remove container and item borders:
<div id="list" class="vlist vlist--borderless"></div>
Striped #
Alternating row backgrounds:
<div id="list" class="vlist vlist--striped"></div>
Animated #
Enable smooth item transitions:
<div id="list" class="vlist vlist--animate"></div>
Combining Variants #
<div id="list" class="vlist vlist--compact vlist--striped vlist--borderless"></div>
Dark Mode #
Automatic (System Preference) #
Dark mode is automatically applied when the user's system prefers dark mode:
@media (prefers-color-scheme: dark) {
:root {
--vlist-bg: #111827;
/* ... dark mode tokens */
}
}
Manual Toggle #
Use the .dark class on any parent element:
<body class="dark">
<div id="list"></div>
</body>
Or scope it to the list:
<div id="list" class="dark"></div>
Tailwind CSS Integration #
If using Tailwind's dark mode:
<!-- Tailwind class-based dark mode -->
<html class="dark">
<body>
<div id="list"></div>
</body>
</html>
Template Styling #
Style items directly in your template function:
const list = vlist({
container: '#app',
item: {
height: 64,
template: (item, index, { selected, focused }) => `
<div class="flex items-center gap-4 w-full">
<img
src="${item.avatar}"
class="w-10 h-10 rounded-full"
alt="${item.name}"
/>
<div class="flex-1 min-w-0">
<div class="font-medium truncate ${selected ? 'text-blue-600' : ''}">${item.name}</div>
<div class="text-sm text-gray-500 truncate">${item.email}</div>
</div>
</div>
`,
},
items: users,
selection: { mode: 'single' }
});
Template Context #
The template function receives useful state:
item: {
height: 48,
template: (item, index, context) => {
const { selected, focused } = context;
// selected: boolean - Is this item selected?
// focused: boolean - Is this item keyboard-focused?
return `...`;
},
}
Loading & Empty States #
Loading Overlay #
.vlist-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
z-index: 20;
}
.vlist-loading-spinner {
width: 2rem;
height: 2rem;
border: 4px solid var(--vlist-border);
border-top-color: var(--vlist-focus-ring);
border-radius: 50%;
animation: vlist-spin 1s linear infinite;
}
@keyframes vlist-spin {
to { transform: rotate(360deg); }
}
Empty State #
.vlist-empty {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--vlist-text-muted);
padding: 2rem;
text-align: center;
}
.vlist-empty-text {
font-size: 1.125rem;
font-weight: 500;
}
.vlist-empty-subtext {
font-size: 0.875rem;
margin-top: 0.25rem;
opacity: 0.75;
}
Animations #
Fade-in Animation #
New items can animate in:
@keyframes vlist-fade-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.vlist-item--enter {
animation: vlist-fade-in 0.2s ease-out;
}
Smooth Transitions #
Enable with the .vlist--animate variant:
.vlist--animate .vlist-item {
transition:
background-color var(--vlist-transition-duration) var(--vlist-transition-timing),
transform 0.2s ease-out,
opacity 0.2s ease-out;
}
Utility Classes #
Hide Scrollbar #
Keep scroll functionality but hide the scrollbar:
<div id="list" class="vlist-scrollbar-hide"></div>
.vlist-scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
}
.vlist-scrollbar-hide::-webkit-scrollbar {
display: none;
}
Best Practices #
1. Use Design Tokens #
Always use CSS custom properties instead of hardcoding values:
/* ✅ Good */
.my-item {
background: var(--vlist-bg-selected);
}
/* ❌ Avoid */
.my-item {
background: #eff6ff;
}
2. Scope Custom Styles #
Use specific selectors to avoid conflicts:
/* ✅ Good - scoped */
#my-list .vlist-item {
font-size: 14px;
}
/* ❌ Avoid - too broad */
.vlist-item {
font-size: 14px;
}
3. Keep Templates Lightweight #
For best performance, keep template CSS minimal:
// ✅ Good - uses existing classes
item: {
height: 48,
template: (item) => `
<div class="flex items-center gap-2">
<span>${item.name}</span>
</div>
`,
}
// ❌ Avoid - complex inline styles
item: {
height: 48,
template: (item) => `
<div style="display:flex;align-items:center;gap:8px;padding:12px;">
<span>${item.name}</span>
</div>
`,
}
4. Test Dark Mode #
Always verify your customizations work in both light and dark modes.
See also: Getting Started | API Reference