@ericrisco/rsc 3.3.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/manifest.json +43 -22
- package/package.json +1 -1
- package/scripts/lib/default-skill-floor.js +5 -1
- package/scripts/lib/domains.js +2 -2
- package/scripts/lib/retired-skills.js +4 -0
- package/skills/design/SKILL.md +4 -3
- package/skills/design/evals/cases.yaml +2 -2
- package/skills/react/SKILL.md +2 -1
- package/skills/ux/SKILL.md +149 -0
- package/skills/ux/THIRD_PARTY.md +32 -0
- package/skills/ux/evals/cases.yaml +40 -0
- package/skills/ux/references/flows.md +124 -0
- package/skills/ux/upstream/UPSTREAM.json +59 -0
- package/skills/ux/upstream/ux-designer/GUIDE.md +180 -0
- package/skills/ux/upstream/ux-designer/LICENSE +21 -0
- package/skills/ux/upstream/ux-designer/references/01-core-principles.md +251 -0
- package/skills/ux/upstream/ux-designer/references/02-laws-of-ux.md +330 -0
- package/skills/ux/upstream/ux-designer/references/03-accessibility.md +411 -0
- package/skills/ux/upstream/ux-designer/references/04-visual-design.md +433 -0
- package/skills/ux/upstream/ux-designer/references/05-information-architecture.md +432 -0
- package/skills/ux/upstream/ux-designer/references/06-interaction-design.md +495 -0
- package/skills/ux/upstream/ux-designer/references/07-forms-and-inputs.md +491 -0
- package/skills/ux/upstream/ux-designer/references/08-mobile-ux.md +539 -0
- package/skills/ux/upstream/ux-designer/references/09-ux-writing.md +493 -0
- package/skills/ux/upstream/ux-designer/references/10-user-research.md +533 -0
- package/skills/ux/upstream/ux-designer/references/11-design-systems.md +618 -0
- package/skills/ux/upstream/ux-designer/references/12a-presence-awareness.md +395 -0
- package/skills/ux/upstream/ux-designer/references/12b-conflict-resolution-sync.md +420 -0
- package/skills/ux/upstream/ux-designer/references/13a-canvas-navigation.md +388 -0
- package/skills/ux/upstream/ux-designer/references/13b-canvas-objects-performance.md +559 -0
- package/skills/ux/upstream/ux-designer/references/14-ai-ux-patterns.md +511 -0
- package/skills/ux/upstream/ux-designer/references/15-ethical-design.md +419 -0
- package/skills/ux/upstream/ux-designer/references/16-onboarding.md +430 -0
- package/skills/ux/upstream/ux-designer/references/17-notifications.md +461 -0
- package/skills/ux/upstream/ux-designer/references/18-data-visualization.md +453 -0
- package/skills/ux/upstream/ux-designer/references/19-search-ux.md +479 -0
- package/skills/ux/upstream/ux-designer/references/20-emotional-design.md +382 -0
- package/skills/ux/upstream/ux-designer/references/21-data-tables.md +629 -0
- package/skills/ux/upstream/ux-designer/references/22-performance-ux.md +626 -0
- package/skills/ux/upstream/ux-designer/references/23-internationalization.md +347 -0
- package/skills/ux/upstream/ux-designer/references/24-voice-and-multimodal.md +308 -0
- package/skills/ux/upstream/ux-designer/references/25a-barcode-camera-scanning.md +342 -0
- package/skills/ux/upstream/ux-designer/references/25b-barcode-scanner-workflows.md +412 -0
- package/skills/ux/upstream/ux-designer/references/25c-barcode-display.md +277 -0
- package/skills/ux/upstream/ux-usability-foundations/GUIDE.md +285 -0
- package/skills/ux/upstream/ux-usability-foundations/LICENSE +21 -0
- package/skills/ux/upstream/ux-usability-foundations/references/anti-patterns.md +267 -0
- package/skills/ux/upstream/ux-usability-foundations/references/checklists.md +130 -0
- package/skills/ux/upstream/ux-usability-foundations/references/decision-prompts.md +340 -0
- package/skills/ux/upstream/ux-usability-foundations/references/principle-cards.md +449 -0
- package/targets/clone-bootstrap.mjs +1 -1
- package/skills/ui-engineering/SKILL.md +0 -113
- package/skills/ui-engineering/evals/cases.yaml +0 -38
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
# Data Tables & Lists
|
|
2
|
+
|
|
3
|
+
Data tables are one of the most common UI patterns in enterprise and admin applications. Well-designed tables help users scan, compare, sort, filter, and act on structured data efficiently.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Table Layout Patterns
|
|
10
|
+
- Sorting
|
|
11
|
+
- Filtering
|
|
12
|
+
- Pagination vs. Infinite Scroll vs. Virtual Scrolling
|
|
13
|
+
- Bulk Selection & Actions
|
|
14
|
+
- Row Expansion & Detail
|
|
15
|
+
- Empty States & Loading
|
|
16
|
+
- Table Toolbar
|
|
17
|
+
- Accessibility
|
|
18
|
+
- Key Metrics
|
|
19
|
+
- Anti-Patterns
|
|
20
|
+
- Sources
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table Layout Patterns
|
|
25
|
+
|
|
26
|
+
### Basic Table Structure
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<table class="data-table" role="grid" aria-label="User accounts">
|
|
30
|
+
<thead>
|
|
31
|
+
<tr>
|
|
32
|
+
<th scope="col" aria-sort="ascending">
|
|
33
|
+
<button class="sort-button">
|
|
34
|
+
Name <span class="sort-icon">↑</span>
|
|
35
|
+
</button>
|
|
36
|
+
</th>
|
|
37
|
+
<th scope="col">Email</th>
|
|
38
|
+
<th scope="col">Role</th>
|
|
39
|
+
<th scope="col">Actions</th>
|
|
40
|
+
</tr>
|
|
41
|
+
</thead>
|
|
42
|
+
<tbody>
|
|
43
|
+
<tr>
|
|
44
|
+
<td>Alice Smith</td>
|
|
45
|
+
<td>alice@example.com</td>
|
|
46
|
+
<td><span class="badge badge--admin">Admin</span></td>
|
|
47
|
+
<td>
|
|
48
|
+
<button class="btn-icon" aria-label="Edit Alice Smith">✏️</button>
|
|
49
|
+
<button class="btn-icon" aria-label="Delete Alice Smith">🗑️</button>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Fixed Headers
|
|
57
|
+
|
|
58
|
+
Keep headers visible while scrolling long tables.
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
.data-table-container {
|
|
62
|
+
max-height: 600px;
|
|
63
|
+
overflow-y: auto;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.data-table thead th {
|
|
67
|
+
position: sticky;
|
|
68
|
+
top: 0;
|
|
69
|
+
z-index: 1;
|
|
70
|
+
background: white;
|
|
71
|
+
box-shadow: 0 1px 0 #e5e7eb;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fixed Columns
|
|
76
|
+
|
|
77
|
+
Keep identifier columns visible during horizontal scroll.
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
.data-table td:first-child,
|
|
81
|
+
.data-table th:first-child {
|
|
82
|
+
position: sticky;
|
|
83
|
+
left: 0;
|
|
84
|
+
z-index: 2;
|
|
85
|
+
background: white;
|
|
86
|
+
box-shadow: 1px 0 0 #e5e7eb;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Corner cell needs highest z-index */
|
|
90
|
+
.data-table thead th:first-child {
|
|
91
|
+
z-index: 3;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Column Resizing
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
function initColumnResize(table) {
|
|
99
|
+
const headers = table.querySelectorAll('th');
|
|
100
|
+
|
|
101
|
+
headers.forEach(header => {
|
|
102
|
+
const resizer = document.createElement('div');
|
|
103
|
+
resizer.className = 'column-resizer';
|
|
104
|
+
header.appendChild(resizer);
|
|
105
|
+
|
|
106
|
+
let startX, startWidth;
|
|
107
|
+
|
|
108
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
109
|
+
startX = e.clientX;
|
|
110
|
+
startWidth = header.offsetWidth;
|
|
111
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
112
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
function handleMouseMove(e) {
|
|
116
|
+
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
|
|
117
|
+
header.style.width = `${newWidth}px`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleMouseUp() {
|
|
121
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
122
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```css
|
|
129
|
+
.column-resizer {
|
|
130
|
+
position: absolute;
|
|
131
|
+
right: 0;
|
|
132
|
+
top: 0;
|
|
133
|
+
bottom: 0;
|
|
134
|
+
width: 4px;
|
|
135
|
+
cursor: col-resize;
|
|
136
|
+
background: transparent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.column-resizer:hover,
|
|
140
|
+
.column-resizer:active {
|
|
141
|
+
background: #3b82f6;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Responsive Table Strategies
|
|
146
|
+
|
|
147
|
+
| Strategy | When to Use |
|
|
148
|
+
|----------|-------------|
|
|
149
|
+
| Horizontal scroll | Many columns, data must stay tabular |
|
|
150
|
+
| Column hiding | Secondary columns can be hidden on mobile |
|
|
151
|
+
| Card layout | Reflow rows into stacked cards below breakpoint |
|
|
152
|
+
| Priority columns | Show only high-priority columns, expand for more |
|
|
153
|
+
|
|
154
|
+
#### Card Reflow Pattern
|
|
155
|
+
|
|
156
|
+
```css
|
|
157
|
+
@media (max-width: 768px) {
|
|
158
|
+
.data-table thead {
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.data-table tr {
|
|
163
|
+
display: block;
|
|
164
|
+
margin-bottom: 12px;
|
|
165
|
+
border: 1px solid #e5e7eb;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
padding: 12px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.data-table td {
|
|
171
|
+
display: flex;
|
|
172
|
+
justify-content: space-between;
|
|
173
|
+
padding: 4px 0;
|
|
174
|
+
border: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.data-table td::before {
|
|
178
|
+
content: attr(data-label);
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
color: #6b7280;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Sorting
|
|
188
|
+
|
|
189
|
+
### Sort Interaction
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
193
|
+
│ Name ↑ │ Email │ Created ↕ │ Status │
|
|
194
|
+
├──────────────────────────────────────────────────────────────┤
|
|
195
|
+
│ Alice Smith │ alice@ex.com │ 2025-01-15 │ Active │
|
|
196
|
+
│ Bob Jones │ bob@ex.com │ 2025-02-01 │ Active │
|
|
197
|
+
│ Carol Lee │ carol@ex.com │ 2024-12-20 │ Inactive │
|
|
198
|
+
└──────────────────────────────────────────────────────────────┘
|
|
199
|
+
|
|
200
|
+
↑ = Ascending (currently active)
|
|
201
|
+
↕ = Sortable but not active
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Sort State Indicators
|
|
205
|
+
|
|
206
|
+
| Symbol | Meaning |
|
|
207
|
+
|--------|---------|
|
|
208
|
+
| ↑ | Sorted ascending |
|
|
209
|
+
| ↓ | Sorted descending |
|
|
210
|
+
| ↕ | Sortable (click to sort) |
|
|
211
|
+
| (none) | Not sortable |
|
|
212
|
+
|
|
213
|
+
#### Best Practices
|
|
214
|
+
|
|
215
|
+
- Click column header to sort ascending, click again for descending, third click to clear
|
|
216
|
+
- Always show sort direction indicator on the active column
|
|
217
|
+
- Use `aria-sort="ascending"` / `aria-sort="descending"` for screen readers
|
|
218
|
+
- Default sort should match the most common use case
|
|
219
|
+
- Support multi-column sort with Shift+Click (show sort priority numbers)
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Filtering
|
|
224
|
+
|
|
225
|
+
### Filter Bar Pattern
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
229
|
+
│ 🔍 Search users... [Status ▼] [Role ▼] [Date range ▼] │
|
|
230
|
+
│ │
|
|
231
|
+
│ Active filters: Status: Active ✕ │ Role: Admin ✕ [Clear] │
|
|
232
|
+
├──────────────────────────────────────────────────────────────┤
|
|
233
|
+
│ Showing 24 of 156 users │
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Filter Types by Data
|
|
237
|
+
|
|
238
|
+
| Data Type | Filter UI |
|
|
239
|
+
|-----------|-----------|
|
|
240
|
+
| Text | Search input (debounced, 300ms) |
|
|
241
|
+
| Enum/Category | Dropdown or checkbox group |
|
|
242
|
+
| Boolean | Toggle switch |
|
|
243
|
+
| Date | Date range picker |
|
|
244
|
+
| Number | Range slider or min/max inputs |
|
|
245
|
+
| Tags | Multi-select with typeahead |
|
|
246
|
+
|
|
247
|
+
### Filter Placement
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Filter placement decision:
|
|
251
|
+
├── Few filters (1-3)?
|
|
252
|
+
│ └── → Inline filter bar above table
|
|
253
|
+
├── Many filters (4-8)?
|
|
254
|
+
│ └── → Collapsible filter panel (sidebar or above table)
|
|
255
|
+
└── Complex filters with dependencies?
|
|
256
|
+
└── → Dedicated filter modal or side panel
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Active Filter Display
|
|
260
|
+
|
|
261
|
+
- Show active filters as dismissible chips/tags
|
|
262
|
+
- Include "Clear all filters" action when any filter is active
|
|
263
|
+
- Show result count that updates as filters change
|
|
264
|
+
- Persist filters in URL params for shareable filtered views
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Pagination vs. Infinite Scroll vs. Virtual Scrolling
|
|
269
|
+
|
|
270
|
+
### Decision Tree
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
How should users navigate through data?
|
|
274
|
+
├── User needs to reach specific pages or find exact position?
|
|
275
|
+
│ └── → Pagination
|
|
276
|
+
├── Content is browsable/exploratory (social feed, catalog)?
|
|
277
|
+
│ ├── Total items < 1,000?
|
|
278
|
+
│ │ └── → Infinite scroll
|
|
279
|
+
│ └── Total items > 1,000?
|
|
280
|
+
│ └── → Infinite scroll with "jump to" option
|
|
281
|
+
├── All data must be in DOM for client-side operations?
|
|
282
|
+
│ └── → Virtual scrolling (windowed rendering)
|
|
283
|
+
└── Data set is very large (10,000+ rows)?
|
|
284
|
+
└── → Server-side pagination OR virtual scrolling
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Pagination
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
291
|
+
│ Showing 1-25 of 156 Rows per page: [25 ▼] │
|
|
292
|
+
│ │
|
|
293
|
+
│ [← Prev] 1 2 3 ... 6 7 [Next →] │
|
|
294
|
+
└──────────────────────────────────────────────────────────────┘
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Pagination best practices:**
|
|
298
|
+
- Show current range ("Showing 1-25 of 156")
|
|
299
|
+
- Allow page size selection (10, 25, 50, 100)
|
|
300
|
+
- Show first/last page links for long ranges
|
|
301
|
+
- Use ellipsis for large page counts
|
|
302
|
+
- Keyboard accessible (arrow keys between pages)
|
|
303
|
+
|
|
304
|
+
### Infinite Scroll
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
const observer = new IntersectionObserver(
|
|
308
|
+
(entries) => {
|
|
309
|
+
if (entries[0].isIntersecting) {
|
|
310
|
+
loadNextPage();
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{ rootMargin: '200px' } // Trigger 200px before reaching end
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
observer.observe(document.querySelector('.load-trigger'));
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Infinite scroll best practices:**
|
|
320
|
+
- Show loading indicator at bottom during fetch
|
|
321
|
+
- Preserve scroll position on back navigation
|
|
322
|
+
- Provide "Back to top" button after scrolling
|
|
323
|
+
- Show total count if known ("Showing 75 of ~500")
|
|
324
|
+
- Combine with a "Load more" button as fallback
|
|
325
|
+
|
|
326
|
+
### Virtual Scrolling
|
|
327
|
+
|
|
328
|
+
Only render rows visible in the viewport.
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
// Virtual scroll concept
|
|
332
|
+
function getVisibleRows(scrollTop, viewportHeight, rowHeight, totalRows) {
|
|
333
|
+
const startIndex = Math.floor(scrollTop / rowHeight);
|
|
334
|
+
const endIndex = Math.min(
|
|
335
|
+
startIndex + Math.ceil(viewportHeight / rowHeight) + 1,
|
|
336
|
+
totalRows
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
startIndex,
|
|
341
|
+
endIndex,
|
|
342
|
+
offsetY: startIndex * rowHeight,
|
|
343
|
+
totalHeight: totalRows * rowHeight
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Virtual scrolling best practices:**
|
|
349
|
+
- Keep row height consistent or measure dynamically
|
|
350
|
+
- Render buffer rows above and below viewport (5-10 rows)
|
|
351
|
+
- Show scroll position indicator for long lists
|
|
352
|
+
- Support keyboard navigation (arrow keys, Page Up/Down)
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Bulk Selection & Actions
|
|
357
|
+
|
|
358
|
+
### Selection Patterns
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
362
|
+
│ ☑ Select all (24 on this page) [Select all 156 →] │
|
|
363
|
+
│ 3 selected [Delete] [Export] [Change status ▼] [✕ Clear] │
|
|
364
|
+
├──────────────────────────────────────────────────────────────┤
|
|
365
|
+
│ ☑ │ Alice Smith │ alice@ex.com │ Active │ │
|
|
366
|
+
│ ☐ │ Bob Jones │ bob@ex.com │ Active │ │
|
|
367
|
+
│ ☑ │ Carol Lee │ carol@ex.com │ Inactive │ │
|
|
368
|
+
│ ☑ │ Dave Park │ dave@ex.com │ Active │ │
|
|
369
|
+
└──────────────────────────────────────────────────────────────┘
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Selection Interactions
|
|
373
|
+
|
|
374
|
+
| Action | Behavior |
|
|
375
|
+
|--------|----------|
|
|
376
|
+
| Click checkbox | Toggle individual row |
|
|
377
|
+
| Shift + Click | Select range from last selected |
|
|
378
|
+
| Header checkbox | Select/deselect all on current page |
|
|
379
|
+
| "Select all N" | Select across all pages |
|
|
380
|
+
|
|
381
|
+
### Bulk Action Bar
|
|
382
|
+
|
|
383
|
+
```css
|
|
384
|
+
.bulk-action-bar {
|
|
385
|
+
position: sticky;
|
|
386
|
+
top: 0;
|
|
387
|
+
z-index: 10;
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 8px;
|
|
391
|
+
padding: 8px 16px;
|
|
392
|
+
background: #eff6ff;
|
|
393
|
+
border-bottom: 1px solid #bfdbfe;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Best practices:**
|
|
398
|
+
- Show bulk action bar only when items are selected
|
|
399
|
+
- Display count of selected items
|
|
400
|
+
- Require confirmation for destructive bulk actions
|
|
401
|
+
- Show progress indicator for long-running bulk operations
|
|
402
|
+
- Allow cancellation of in-progress bulk operations
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Row Expansion & Detail
|
|
407
|
+
|
|
408
|
+
### Expandable Rows
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
412
|
+
│ ▶ │ Order #1234 │ Alice Smith │ $149.00 │ Shipped │
|
|
413
|
+
│ ▼ │ Order #1235 │ Bob Jones │ $89.50 │ Processing │
|
|
414
|
+
│ ├──────────────────────────────────────────────────────────┤
|
|
415
|
+
│ │ Items: Widget A (×2), Gadget B (×1) │
|
|
416
|
+
│ │ Tracking: 1Z999AA10123456784 │
|
|
417
|
+
│ │ Estimated delivery: Jan 20, 2025 │
|
|
418
|
+
│ ├──────────────────────────────────────────────────────────┤
|
|
419
|
+
│ ▶ │ Order #1236 │ Carol Lee │ $250.00 │ Delivered │
|
|
420
|
+
└──────────────────────────────────────────────────────────────┘
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Best practices:**
|
|
424
|
+
- Use chevron (▶/▼) to indicate expandability
|
|
425
|
+
- Expand only one row at a time (accordion) or allow multiple
|
|
426
|
+
- Keep expanded content visually connected to the parent row
|
|
427
|
+
- Indent or inset expanded content for visual hierarchy
|
|
428
|
+
|
|
429
|
+
### Inline Editing
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
function enableInlineEdit(cell) {
|
|
433
|
+
const currentValue = cell.textContent;
|
|
434
|
+
const input = document.createElement('input');
|
|
435
|
+
input.value = currentValue;
|
|
436
|
+
input.className = 'inline-edit-input';
|
|
437
|
+
|
|
438
|
+
cell.replaceChildren(input);
|
|
439
|
+
input.focus();
|
|
440
|
+
input.select();
|
|
441
|
+
|
|
442
|
+
input.addEventListener('blur', () => saveEdit(cell, input.value, currentValue));
|
|
443
|
+
input.addEventListener('keydown', (e) => {
|
|
444
|
+
if (e.key === 'Enter') input.blur();
|
|
445
|
+
if (e.key === 'Escape') {
|
|
446
|
+
input.value = currentValue;
|
|
447
|
+
input.blur();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```css
|
|
454
|
+
.inline-edit-input {
|
|
455
|
+
width: 100%;
|
|
456
|
+
padding: 4px 8px;
|
|
457
|
+
border: 2px solid #3b82f6;
|
|
458
|
+
border-radius: 4px;
|
|
459
|
+
font: inherit;
|
|
460
|
+
background: #eff6ff;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Inline edit guidelines:**
|
|
465
|
+
- Double-click or click edit icon to activate
|
|
466
|
+
- Show clear visual change (border, background) when in edit mode
|
|
467
|
+
- Save on blur or Enter, cancel on Escape
|
|
468
|
+
- Validate before saving, show errors inline
|
|
469
|
+
- Show saving indicator (spinner) during async save
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Empty States & Loading
|
|
474
|
+
|
|
475
|
+
### Empty State Patterns
|
|
476
|
+
|
|
477
|
+
```
|
|
478
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
479
|
+
│ │
|
|
480
|
+
│ 📋 │
|
|
481
|
+
│ No results found │
|
|
482
|
+
│ │
|
|
483
|
+
│ Your search "xyz" didn't match any users. │
|
|
484
|
+
│ Try adjusting your filters or search terms. │
|
|
485
|
+
│ │
|
|
486
|
+
│ [Clear filters] [Add new user] │
|
|
487
|
+
│ │
|
|
488
|
+
└──────────────────────────────────────────────────────────────┘
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
| Empty State Type | Message Pattern |
|
|
492
|
+
|-----------------|-----------------|
|
|
493
|
+
| No data yet | Explain what will appear + CTA to create first item |
|
|
494
|
+
| No search results | Acknowledge query + suggest adjustments |
|
|
495
|
+
| Filtered to empty | Show active filters + "Clear filters" button |
|
|
496
|
+
| Error loading | Explain what happened + "Retry" button |
|
|
497
|
+
|
|
498
|
+
### Loading Skeletons
|
|
499
|
+
|
|
500
|
+
```css
|
|
501
|
+
.skeleton-row {
|
|
502
|
+
display: flex;
|
|
503
|
+
gap: 16px;
|
|
504
|
+
padding: 12px 16px;
|
|
505
|
+
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.skeleton-cell {
|
|
509
|
+
height: 16px;
|
|
510
|
+
background: #e5e7eb;
|
|
511
|
+
border-radius: 4px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.skeleton-cell--name { width: 150px; }
|
|
515
|
+
.skeleton-cell--email { width: 200px; }
|
|
516
|
+
.skeleton-cell--status { width: 80px; }
|
|
517
|
+
|
|
518
|
+
@keyframes skeleton-pulse {
|
|
519
|
+
0%, 100% { opacity: 1; }
|
|
520
|
+
50% { opacity: 0.4; }
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**Loading best practices:**
|
|
525
|
+
- Show skeleton that matches table structure (column widths)
|
|
526
|
+
- Show 5-10 skeleton rows regardless of expected count
|
|
527
|
+
- Transition smoothly from skeleton to real data
|
|
528
|
+
- Keep headers visible during loading
|
|
529
|
+
- Show spinner only for subsequent page loads, not initial
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Table Toolbar
|
|
534
|
+
|
|
535
|
+
### Standard Toolbar Layout
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
539
|
+
│ Users (156) │
|
|
540
|
+
│ │
|
|
541
|
+
│ 🔍 Search... [Status ▼] [Role ▼] │ [Export ▼] [+ Add] │
|
|
542
|
+
│ │
|
|
543
|
+
│ Active filters: Status: Active ✕ │ Showing 1-25 of 156 │
|
|
544
|
+
├──────────────────────────────────────────────────────────────┤
|
|
545
|
+
│ ☐ │ Name ↕ │ Email │ Role │ Status │ ⋮ │
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Column Visibility Toggle
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
┌─────────────────────────────┐
|
|
552
|
+
│ Columns │
|
|
553
|
+
├─────────────────────────────┤
|
|
554
|
+
│ ☑ Name │
|
|
555
|
+
│ ☑ Email │
|
|
556
|
+
│ ☑ Role │
|
|
557
|
+
│ ☐ Created date │
|
|
558
|
+
│ ☐ Last login │
|
|
559
|
+
│ ☑ Status │
|
|
560
|
+
├─────────────────────────────┤
|
|
561
|
+
│ [Reset to default] │
|
|
562
|
+
└─────────────────────────────┘
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Accessibility
|
|
568
|
+
|
|
569
|
+
### Table Accessibility Checklist
|
|
570
|
+
|
|
571
|
+
- [ ] Use semantic `<table>`, `<thead>`, `<tbody>`, `<th>`, `<td>` elements
|
|
572
|
+
- [ ] Add `scope="col"` to column headers, `scope="row"` to row headers
|
|
573
|
+
- [ ] Use `aria-sort` on sortable column headers
|
|
574
|
+
- [ ] Provide `aria-label` or `aria-labelledby` on the table
|
|
575
|
+
- [ ] Ensure keyboard navigation works (Tab between interactive elements)
|
|
576
|
+
- [ ] Row actions accessible via keyboard (Enter/Space to activate)
|
|
577
|
+
- [ ] Bulk selection announced to screen readers ("3 rows selected")
|
|
578
|
+
- [ ] Pagination controls are keyboard accessible
|
|
579
|
+
- [ ] Sort changes announced via `aria-live` region
|
|
580
|
+
|
|
581
|
+
### Keyboard Navigation
|
|
582
|
+
|
|
583
|
+
| Key | Action |
|
|
584
|
+
|-----|--------|
|
|
585
|
+
| Tab | Move to next interactive element |
|
|
586
|
+
| Shift + Tab | Move to previous interactive element |
|
|
587
|
+
| Enter / Space | Activate button, toggle checkbox, start inline edit |
|
|
588
|
+
| Escape | Cancel inline edit, close dropdown |
|
|
589
|
+
| Arrow keys | Navigate within dropdowns or between rows (if using `role="grid"`) |
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Key Metrics
|
|
594
|
+
|
|
595
|
+
| Metric | Value | Context |
|
|
596
|
+
|--------|-------|---------|
|
|
597
|
+
| Rows per page default | 25 | Balance between scanning and loading |
|
|
598
|
+
| Max visible columns | 7-10 | Before horizontal scroll or hiding |
|
|
599
|
+
| Search debounce | 300ms | Delay before triggering search |
|
|
600
|
+
| Skeleton rows | 5-10 | During initial load |
|
|
601
|
+
| Column min width | 60px | Minimum resizable width |
|
|
602
|
+
| Row height | 40-52px | Comfortable click/tap target |
|
|
603
|
+
| Bulk action confirm | Always | For destructive operations |
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Anti-Patterns
|
|
608
|
+
|
|
609
|
+
1. **No sort indicators** - Users can't tell which column is sorted or in which direction
|
|
610
|
+
2. **Client-side sort on server-paginated data** - Sorts only the visible page, not the full dataset
|
|
611
|
+
3. **Pagination without count** - Users don't know how much data exists
|
|
612
|
+
4. **Tiny action buttons** - Hard to click, especially on touch devices
|
|
613
|
+
5. **No empty state** - Blank table with headers but no guidance
|
|
614
|
+
6. **Inline editing without undo** - Users can't recover from accidental changes
|
|
615
|
+
7. **Checkbox column without bulk actions** - Selection exists but does nothing
|
|
616
|
+
8. **Filter resets on navigation** - Losing filter state when leaving and returning
|
|
617
|
+
9. **No loading state between pages** - Content jumps, users lose context
|
|
618
|
+
10. **Forced horizontal scroll on mobile** - Use card reflow instead
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Sources
|
|
623
|
+
|
|
624
|
+
- [Baymard Institute - Data Table Usability](https://baymard.com/blog/data-table-design)
|
|
625
|
+
- [Nielsen Norman Group - Data Tables](https://www.nngroup.com/articles/data-tables/)
|
|
626
|
+
- [Smashing Magazine - Design Patterns for Complex Data Tables](https://www.smashingmagazine.com/2019/02/complex-web-tables/)
|
|
627
|
+
- [TanStack Table](https://tanstack.com/table/) - Headless table library
|
|
628
|
+
- [AG Grid - Best Practices](https://www.ag-grid.com/javascript-data-grid/) - Enterprise grid reference
|
|
629
|
+
- [Ant Design - Table Component](https://ant.design/components/table) - Pattern reference
|