RowSelectionCheckbox
Flexible checkbox component for row selection with master/detail checkbox support.
Overview
The RowSelectionCheckbox component manages row selection state. It supports both individual row selection and a master "select all" checkbox with indeterminate state support.
Best for: Implementing row selection UI in tables and lists
Key Features:
- ✅ Individual row selection
- ✅ Master checkbox with indeterminate state
- ✅ Select all / deselect all functionality
- ✅ Full keyboard support
- ✅ Accessibility labels (ARIA)
- ✅ Disabled state support
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
rowId | string | - | ✅ Yes | Unique identifier for this row (use "master" for the master checkbox) |
isSelected | boolean | false | ✅ Yes | Whether this checkbox is checked |
isMaster | boolean | false | - | Whether this is the master "select all" checkbox |
disabled | boolean | false | - | Whether the checkbox is disabled |
totalRows | number | - | - | Total number of rows (used for master checkbox indeterminate calc) |
selectedCount | number | - | - | Number of selected rows (used for master checkbox indeterminate state) |
Events
| Event | Payload | Description |
|---|---|---|
toggle | [rowId: string, isSelected: boolean] | Individual row checkbox toggled |
toggle-all | [isSelected: boolean] | Master checkbox toggled (select all) |
Basic Usage
Individual Row Selection
<template>
<div class="table">
<div v-for="row in rows" :key="row.id" class="row">
<RowSelectionCheckbox
:row-id="String(row.id)"
:is-selected="selectedRows.has(row.id)"
@toggle="(rowId, isSelected) => toggleRow(rowId, isSelected)"
/>
<span>{{ row.name }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'
const rows = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
const selectedRows = ref(new Set<number>())
function toggleRow(rowId: string, isSelected: boolean) {
const id = parseInt(rowId)
if (isSelected) {
selectedRows.value.add(id)
} else {
selectedRows.value.delete(id)
}
}
</script>Master Checkbox (Select All)
<template>
<div>
<!-- Master checkbox in header -->
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="allSelected"
:total-rows="rows.length"
:selected-count="selectedRows.size"
@toggle-all="toggleSelectAll"
/>
<!-- Individual row checkboxes -->
<div v-for="row in rows" :key="row.id" class="row">
<RowSelectionCheckbox
:row-id="String(row.id)"
:is-selected="selectedRows.has(row.id)"
@toggle="(rowId, selected) => toggleRow(rowId, selected)"
/>
<span>{{ row.name }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'
const rows = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
const selectedRows = ref(new Set<number>())
const allSelected = computed(() => selectedRows.value.size === rows.value.length)
function toggleSelectAll(isSelected: boolean) {
selectedRows.value.clear()
if (isSelected) {
rows.value.forEach(row => selectedRows.value.add(row.id))
}
}
function toggleRow(rowId: string, isSelected: boolean) {
const id = parseInt(rowId)
if (isSelected) {
selectedRows.value.add(id)
} else {
selectedRows.value.delete(id)
}
}
</script>Master Checkbox Indeterminate State
The master checkbox shows three states:
- Unchecked - No rows selected
- Indeterminate - Some rows selected (✓ but not filled)
- Checked - All rows selected
// The component automatically calculates indeterminate state
// when totalRows and selectedCount are provided
// Unchecked: selectedCount = 0
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="false"
:total-rows="10"
:selected-count="0"
/>
// Indeterminate: selectedCount > 0 && selectedCount < totalRows
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="true" // Still true even in indeterminate
:total-rows="10"
:selected-count="5" // Shows indeterminate visually
/>
// Checked: selectedCount = totalRows
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="true"
:total-rows="10"
:selected-count="10"
/>Accessibility
Aria Labels
Component automatically generates appropriate ARIA labels:
// Individual row
aria-label="Select row 123"
// Master checkbox
aria-label="Select all rows"Keyboard Support
- Tab/Shift+Tab - Navigate to checkbox
- Space/Enter - Toggle checkbox
- Full keyboard navigation - No mouse required
Disabled State
<template>
<!-- Disabled individual checkbox -->
<RowSelectionCheckbox
row-id="123"
:is-selected="false"
:disabled="true"
/>
<!-- Disabled master checkbox -->
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="false"
:disabled="true"
/>
</template>Advanced Examples
With Pagination
Handle selection across paginated data:
<script setup lang="ts">
const currentPageRows = ref([...])
const selectedRows = ref(new Set())
// Track selections across all pages
const pageSelectionsMap = ref(new Map())
function toggleRow(rowId: string, isSelected: boolean) {
if (isSelected) {
selectedRows.value.add(rowId)
} else {
selectedRows.value.delete(rowId)
}
// Save current page selections
pageSelectionsMap.value.set(currentPage.value, selectedRows.value)
}
function onPageChange(newPage) {
currentPage.value = newPage
// Restore selections for new page
selectedRows.value = pageSelectionsMap.value.get(newPage) || new Set()
}
</script>Conditional Selection
Disable selection for certain rows:
<template>
<RowSelectionCheckbox
:row-id="String(row.id)"
:is-selected="selectedRows.has(row.id)"
:disabled="!isSelectable(row)"
@toggle="(rowId, selected) => toggleRow(rowId, selected)"
/>
</template>
<script setup lang="ts">
function isSelectable(row: any): boolean {
// Can't select archived rows
return row.status !== 'archived'
}
</script>Real-World Example: Invoice Selection
This example shows how to use RowSelectionCheckbox with realistic invoice data to select multiple invoices for bulk approval operations.
Data Model
interface Invoice {
id: string
documentNumber: string
vendor: string
amount: number
currency: string
invoiceDate: string
status: 'pending' | 'approved' | 'rejected' | 'processed'
assignee: string
extractionConfidence: number
}
const invoices = [
{
id: 'INV-2024-001',
documentNumber: 'INV-001234',
vendor: 'Acme Corporation',
amount: 1250.00,
currency: 'USD',
invoiceDate: '2024-01-15',
status: 'pending',
assignee: 'john.doe@company.com',
extractionConfidence: 0.98
},
{
id: 'INV-2024-002',
documentNumber: 'INV-001235',
vendor: 'Global Supplies Inc',
amount: 3750.50,
currency: 'USD',
invoiceDate: '2024-01-16',
status: 'pending',
assignee: 'jane.smith@company.com',
extractionConfidence: 0.95
},
{
id: 'INV-2024-003',
documentNumber: 'RE-2024-789',
vendor: 'Tech Solutions GmbH',
amount: 2100.00,
currency: 'EUR',
invoiceDate: '2024-01-18',
status: 'pending',
assignee: 'mike.johnson@company.com',
extractionConfidence: 0.97
},
{
id: 'INV-2024-004',
documentNumber: 'INV-UK-445',
vendor: 'British Office Ltd',
amount: 890.00,
currency: 'GBP',
invoiceDate: '2024-01-20',
status: 'pending',
assignee: 'sarah.williams@company.com',
extractionConfidence: 0.89
},
{
id: 'INV-2024-005',
documentNumber: 'F-2024-1122',
vendor: 'Consulting Partners SA',
amount: 8500.00,
currency: 'EUR',
invoiceDate: '2024-01-22',
status: 'pending',
assignee: 'pierre.dubois@company.com',
extractionConfidence: 0.93
},
{
id: 'INV-2024-006',
documentNumber: 'INV-2024-556',
vendor: 'Cloud Services Corp',
amount: 4200.00,
currency: 'USD',
invoiceDate: '2024-01-25',
status: 'approved',
assignee: 'john.doe@company.com',
extractionConfidence: 0.99
},
{
id: 'INV-2024-007',
documentNumber: 'BILL-7788',
vendor: 'Utilities Company',
amount: 650.25,
currency: 'USD',
invoiceDate: '2024-01-28',
status: 'approved',
assignee: 'accounts@company.com',
extractionConfidence: 0.96
},
{
id: 'INV-2024-008',
documentNumber: 'INV-Q1-2024',
vendor: 'Marketing Agency LLC',
amount: 12000.00,
currency: 'USD',
invoiceDate: '2024-01-30',
status: 'pending',
assignee: 'marketing@company.com',
extractionConfidence: 0.91
}
]Implementation with Master Checkbox
<template>
<div class="invoice-selection">
<!-- Header with master checkbox -->
<div class="selection-header">
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="allInvoicesSelected"
:total-rows="invoices.length"
:selected-count="selectedInvoices.size"
@toggle-all="toggleSelectAllInvoices"
/>
<span class="header-label">Select Invoices ({{ selectedInvoices.size }} selected)</span>
<button
v-if="selectedInvoices.size > 0"
class="bulk-approve-btn"
@click="approveSelectedInvoices"
>
Approve Selected
</button>
</div>
<!-- Invoice rows with individual checkboxes -->
<div class="invoice-list">
<div v-for="invoice in invoices" :key="invoice.id" class="invoice-row">
<RowSelectionCheckbox
:row-id="invoice.id"
:is-selected="selectedInvoices.has(invoice.id)"
:disabled="invoice.status === 'approved'"
@toggle="(rowId, selected) => toggleInvoice(rowId, selected)"
/>
<div class="invoice-details">
<div class="vendor-info">
<strong>{{ invoice.vendor }}</strong>
<span class="doc-number">{{ invoice.documentNumber }}</span>
</div>
<div class="amount-info">
{{ invoice.currency }} {{ invoice.amount.toFixed(2) }}
</div>
<div class="meta-info">
<span>{{ new Date(invoice.invoiceDate).toLocaleDateString() }}</span>
<span :class="`status status-${invoice.status}`">
{{ invoice.status.replace('_', ' ').toUpperCase() }}
</span>
<span class="confidence">{{ (invoice.extractionConfidence * 100).toFixed(0) }}% confidence</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'
const invoices = ref([
// ... invoice data from above ...
])
const selectedInvoices = ref(new Set<string>())
const allInvoicesSelected = computed(() => {
const selectableCount = invoices.value.filter(inv => inv.status !== 'approved').length
return selectedInvoices.value.size === selectableCount && selectableCount > 0
})
function toggleSelectAllInvoices(isSelected: boolean) {
selectedInvoices.value.clear()
if (isSelected) {
invoices.value.forEach(invoice => {
// Only select non-approved invoices
if (invoice.status !== 'approved') {
selectedInvoices.value.add(invoice.id)
}
})
}
}
function toggleInvoice(invoiceId: string, isSelected: boolean) {
if (isSelected) {
selectedInvoices.value.add(invoiceId)
} else {
selectedInvoices.value.delete(invoiceId)
}
}
function approveSelectedInvoices() {
const selected = Array.from(selectedInvoices.value)
console.log('Approving invoices:', selected)
// Make API call to approve selected invoices
// Then clear selection
selectedInvoices.value.clear()
}
</script>
<style scoped>
.invoice-selection {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.selection-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.header-label {
flex: 1;
font-weight: 500;
}
.bulk-approve-btn {
padding: 8px 16px;
background: #2388AE;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.invoice-list {
max-height: 500px;
overflow-y: auto;
}
.invoice-row {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
transition: background 0.2s;
}
.invoice-row:hover {
background: #fafafa;
}
.invoice-details {
flex: 1;
}
.vendor-info {
display: flex;
gap: 12px;
align-items: baseline;
margin-bottom: 4px;
}
.doc-number {
color: #666;
font-size: 0.9em;
}
.amount-info {
font-weight: 600;
font-size: 1.1em;
margin-bottom: 4px;
}
.meta-info {
display: flex;
gap: 16px;
font-size: 0.85em;
color: #666;
}
.status {
padding: 2px 8px;
border-radius: 3px;
font-size: 0.75em;
font-weight: 600;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-approved {
background: #d4edda;
color: #155724;
}
.confidence {
color: #2388AE;
}
</style>What This Example Shows
- Master Checkbox: Select/deselect all pending invoices at once
- Conditional Selection: Approved invoices cannot be selected (disabled state)
- Indeterminate State: Master checkbox shows partial selection state
- Bulk Actions: Display action button when invoices are selected
- Real Data: Mix of currencies (USD, EUR, GBP) and statuses
- Selection Count: Dynamic feedback showing number of selected items
Real-World Example: Document Queue Selection
This example demonstrates selecting multiple documents from a processing queue for batch operations.
Data Model
interface Document {
id: string
fileName: string
documentType: 'Invoice' | 'PO' | 'Receipt' | 'Contract'
uploadDate: string
pages: number
processingStatus: 'queued' | 'processing' | 'completed' | 'failed'
ocrConfidence: number
extractedFields: number
}
const documents = [
{
id: 'DOC-2024-001',
fileName: 'invoice_acme_jan2024.pdf',
documentType: 'Invoice',
uploadDate: '2024-01-15T10:30:00Z',
pages: 3,
processingStatus: 'completed',
ocrConfidence: 0.96,
extractedFields: 15
},
{
id: 'DOC-2024-002',
fileName: 'po_global_supply.pdf',
documentType: 'PO',
uploadDate: '2024-01-16T14:22:00Z',
pages: 2,
processingStatus: 'completed',
ocrConfidence: 0.98,
extractedFields: 12
},
{
id: 'DOC-2024-003',
fileName: 'receipt_office_depot.jpg',
documentType: 'Receipt',
uploadDate: '2024-01-17T09:15:00Z',
pages: 1,
processingStatus: 'completed',
ocrConfidence: 0.92,
extractedFields: 8
},
{
id: 'DOC-2024-004',
fileName: 'contract_tech_solutions.pdf',
documentType: 'Contract',
uploadDate: '2024-01-18T16:45:00Z',
pages: 5,
processingStatus: 'processing',
ocrConfidence: 0.0,
extractedFields: 0
},
{
id: 'DOC-2024-005',
fileName: 'invoice_cloud_services.pdf',
documentType: 'Invoice',
uploadDate: '2024-01-19T11:20:00Z',
pages: 1,
processingStatus: 'completed',
ocrConfidence: 0.99,
extractedFields: 14
},
{
id: 'DOC-2024-006',
fileName: 'po_british_office.pdf',
documentType: 'PO',
uploadDate: '2024-01-20T13:30:00Z',
pages: 3,
processingStatus: 'queued',
ocrConfidence: 0.0,
extractedFields: 0
},
{
id: 'DOC-2024-007',
fileName: 'receipt_utilities.pdf',
documentType: 'Receipt',
uploadDate: '2024-01-21T08:00:00Z',
pages: 1,
processingStatus: 'failed',
ocrConfidence: 0.45,
extractedFields: 3
},
{
id: 'DOC-2024-008',
fileName: 'invoice_consulting_partners.pdf',
documentType: 'Invoice',
uploadDate: '2024-01-22T15:10:00Z',
pages: 2,
processingStatus: 'completed',
ocrConfidence: 0.91,
extractedFields: 13
}
]Implementation with Batch Operations
<template>
<div class="document-queue">
<!-- Header with master checkbox -->
<div class="queue-header">
<RowSelectionCheckbox
row-id="master"
:is-master="true"
:is-selected="allDocumentsSelected"
:total-rows="selectableDocuments.length"
:selected-count="selectedDocuments.size"
@toggle-all="toggleSelectAllDocuments"
/>
<span class="header-label">Processing Queue ({{ selectedDocuments.size }} selected)</span>
<div v-if="selectedDocuments.size > 0" class="batch-actions">
<button class="action-btn export" @click="exportSelectedDocuments">
📥 Export
</button>
<button class="action-btn delete" @click="deleteSelectedDocuments">
🗑️ Delete
</button>
</div>
</div>
<!-- Document queue rows -->
<div class="document-queue-list">
<div v-for="doc in documents" :key="doc.id" class="document-row">
<RowSelectionCheckbox
:row-id="doc.id"
:is-selected="selectedDocuments.has(doc.id)"
:disabled="doc.processingStatus === 'processing'"
@toggle="(rowId, selected) => toggleDocument(rowId, selected)"
/>
<div class="document-icon">
{{ getDocumentTypeIcon(doc.documentType) }}
</div>
<div class="document-info">
<div class="file-name">{{ doc.fileName }}</div>
<div class="file-meta">
<span class="type">{{ doc.documentType }}</span>
<span class="pages">{{ doc.pages }} page(s)</span>
<span class="uploaded">{{ formatDate(doc.uploadDate) }}</span>
</div>
</div>
<div class="status-indicator">
<div :class="`status-badge ${doc.processingStatus}`">
{{ formatStatus(doc.processingStatus) }}
</div>
<div v-if="doc.processingStatus === 'completed'" class="confidence">
{{ (doc.ocrConfidence * 100).toFixed(0) }}% confidence
</div>
<div v-else-if="doc.processingStatus === 'processing'" class="processing">
Processing...
</div>
<div v-else-if="doc.processingStatus === 'failed'" class="failed">
{{ doc.extractedFields }} fields extracted
</div>
<div v-else class="queued">
Queued
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'
const documents = ref([
// ... document data from above ...
])
const selectedDocuments = ref(new Set<string>())
const selectableDocuments = computed(() =>
documents.value.filter(doc => doc.processingStatus !== 'processing')
)
const allDocumentsSelected = computed(() => {
return selectedDocuments.value.size === selectableDocuments.value.length &&
selectableDocuments.value.length > 0
})
function toggleSelectAllDocuments(isSelected: boolean) {
selectedDocuments.value.clear()
if (isSelected) {
selectableDocuments.value.forEach(doc => {
selectedDocuments.value.add(doc.id)
})
}
}
function toggleDocument(docId: string, isSelected: boolean) {
if (isSelected) {
selectedDocuments.value.add(docId)
} else {
selectedDocuments.value.delete(docId)
}
}
function exportSelectedDocuments() {
const selected = Array.from(selectedDocuments.value)
console.log('Exporting documents:', selected)
// Make API call to export
selectedDocuments.value.clear()
}
function deleteSelectedDocuments() {
const selected = Array.from(selectedDocuments.value)
if (confirm(`Delete ${selected.length} document(s)?`)) {
console.log('Deleting documents:', selected)
// Make API call to delete
selectedDocuments.value.clear()
}
}
function getDocumentTypeIcon(type: string): string {
const icons: Record<string, string> = {
Invoice: '📄',
PO: '📋',
Receipt: '🧾',
Contract: '📑'
}
return icons[type] || '📄'
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString()
}
function formatStatus(status: string): string {
return status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ')
}
</script>
<style scoped>
.document-queue {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.queue-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.header-label {
flex: 1;
font-weight: 500;
}
.batch-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.action-btn.export {
background: #34eea9;
color: white;
}
.action-btn.delete {
background: #ff6b6b;
color: white;
}
.document-queue-list {
max-height: 600px;
overflow-y: auto;
}
.document-row {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
transition: background 0.2s;
}
.document-row:hover {
background: #fafafa;
}
.document-icon {
font-size: 1.5em;
width: 32px;
text-align: center;
}
.document-info {
flex: 1;
}
.file-name {
font-weight: 500;
margin-bottom: 4px;
}
.file-meta {
display: flex;
gap: 12px;
font-size: 0.85em;
color: #666;
}
.status-indicator {
text-align: right;
min-width: 150px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 0.85em;
font-weight: 600;
margin-bottom: 4px;
}
.status-badge.completed {
background: #d4edda;
color: #155724;
}
.status-badge.queued {
background: #e2e3e5;
color: #383d41;
}
.status-badge.processing {
background: #fff3cd;
color: #856404;
}
.status-badge.failed {
background: #f8d7da;
color: #721c24;
}
.confidence {
font-size: 0.85em;
color: #2388AE;
font-weight: 500;
}
.processing,
.queued {
font-size: 0.85em;
color: #666;
}
.failed {
font-size: 0.85em;
color: #ff6b6b;
}
</style>What This Example Shows
- Document Types: Icons and labels for different document types (Invoice, PO, Receipt, Contract)
- Processing States: Different visual feedback for queued, processing, completed, and failed documents
- Disabled During Processing: Cannot select documents that are actively being processed
- Batch Export/Delete: Quick access to batch operations when documents are selected
- OCR Quality Metrics: Display confidence scores and extracted field counts
- Real Data: Realistic file names and timestamps
- Selective Activation: Master checkbox only counts selectable documents (excludes processing ones)
Testing
Unit Test Example
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import RowSelectionCheckbox from './RowSelectionCheckbox.vue'
describe('RowSelectionCheckbox', () => {
it('renders unchecked by default', () => {
const wrapper = mount(RowSelectionCheckbox, {
props: {
rowId: '1',
isSelected: false
}
})
const checkbox = wrapper.find('input[type="checkbox"]')
expect(checkbox.element).not.toBeChecked()
})
it('emits toggle event when clicked', async () => {
const wrapper = mount(RowSelectionCheckbox, {
props: {
rowId: '1',
isSelected: false
}
})
const checkbox = wrapper.find('input')
await checkbox.trigger('click')
expect(wrapper.emitted('toggle')).toBeTruthy()
expect(wrapper.emitted('toggle')[0]).toEqual(['1', true])
})
it('shows indeterminate state correctly', () => {
const wrapper = mount(RowSelectionCheckbox, {
props: {
rowId: 'master',
isMaster: true,
isSelected: true,
totalRows: 10,
selectedCount: 5
}
})
const checkbox = wrapper.find('input')
expect(checkbox.element).toHaveProperty('indeterminate', true)
})
})Browser Support
Supported in all modern browsers:
- Chrome 88+
- Firefox 78+
- Safari 14+
- Edge 88+
Related Components
- DocBitsTable - Complete table with selection
- BulkActionsMenu - Actions for selected rows
- ColumnOrderBy - Column sorting
Source Code
- Component:
src/components/Table/RowSelectionCheckbox.vue(83 lines) - Tests: 42 component tests, 30+ unit tests
Changelog
v1.0.0 (2025-12-18)
- Initial release
- Individual and master checkbox support
- Indeterminate state for partial selection
- Full accessibility support
- Keyboard navigation