This document describes the implementation of accessible, discoverable filter chips for the Transaction History page (#132), including type, date-range, and amount-range filtering with distinct empty/no-results UX states.
- Options: All, Draw, Repay, Fee, Interest (excludes StatusChange type)
- Implementation: Toggle button group with
role="group"andaria-labelledby - Accessibility:
aria-pressed="true/false"indicates current toggle state- Keyboard navigation: Tab to focus, Space/Enter to activate
- Visual feedback: Active state shows accent color with shadow
- Works with any screen reader via ARIA attributes
- Presets: Today, 7d, 30d, 90d, Custom
- Implementation: Same toggle button group pattern as type filters
- Efficiency: Presets provide quick access to common time ranges
- Accessibility: Identical to type filters (role, aria-pressed, keyboard support)
- Quick chips: All amounts, Under $5k, $5k-$25k, $25k+
- Custom flow: Separate mobile-friendly modal for fine-grained min/max filtering
- Implementation: Reuses the transaction filter chip styling and project modal accessibility hooks
- Accessibility:
- Quick chips expose
aria-pressedstate like the other filter groups - Custom trigger exposes
aria-haspopup="dialog"andaria-pressed - Dialog traps focus, locks background scrolling, and makes background content inert
- All controls meet the 44px touch-target requirement and keep a visible focus ring
- Quick chips expose
The result count is displayed in an aria-live="polite" region that:
- Updates in real-time as filters change
- Uses
aria-atomic="true"to ensure full announcement - Shows proper pluralization: "1 transaction shown" vs "3 transactions shown"
- Announces to screen readers without interrupting user workflow
Example announcements:
- "28 transactions shown" (initial state)
- "3 transactions shown" (when 7d filter applied)
- "0 transactions shown" (when filters produce no results)
Three clear, separate UX patterns for data absence:
- Condition:
!hasLines - Message: "No credit lines yet"
- Action: Link to "Request Credit Evaluation"
- Icon: π
- Use case: First-time users with no credit applications
- Condition:
!hasTransactions - Message: "No transactions yet"
- Description: Explains transactions will appear after activity
- Icon: π
- Use case: Users with credit lines but no transaction history
- Condition:
filteredTransactions.length === 0 - Message: "No transactions match these filters"
- Description: Suggests modifying filters
- Action: "Clear filters" button (only when filters are active)
- Icon: π
- Use case: Active filters return zero matches
Distinction Logic:
const hasActiveFilters =
selectedLine !== "all" ||
selectedType !== "all" ||
selectedStatus !== "all" ||
dateRange !== "all" ||
searchQuery.trim().length > 0;
// "Clear filters" button only shows when:
// 1. No results exist (filteredTransactions.length === 0)
// 2. AND at least one filter is active (hasActiveFilters === true)Filters are applied in cascading order:
- Credit Line Filter - Select by lineId
- Transaction Type Filter - Filter by type (Draw, Repay, Fee, Interest, StatusChange)
- Status Filter - Filter by transaction status (Completed, Pending, Failed)
- Amount Range Filter - Filter by quick amount chips or custom min/max bounds
- Date Range Filter - Calculate cutoff time from preset days (Today, 7d, 30d, 90d) or custom dates
- Search Query - Full-text search across note, lineName, lineId, txHash
Each filter change:
- Resets pagination to page 1 (prevents disorientation)
- Triggers result count announcement
- Updates aria-live region
| Key | Action |
|---|---|
| Tab | Navigate between filter chips and other controls |
| Shift+Tab | Navigate backwards |
| Space | Toggle the focused filter chip |
| Enter | Toggle the focused filter chip (alternative to Space) |
| Arrow Keys | Not implemented (follows WAI-ARIA button group pattern) |
- β All filter chips have proper roles (button)
- β aria-pressed states clearly identify toggle values
- β Accessible names provided via button labels
- β Screen readers announce changes via aria-live region
- β Chips are in logical focus order (left to right, top to bottom)
- β Focus is visible with :focus-visible outline
- β Focus management resets to top of page on state change
- β Filter chips use consistent visual language
- β Active/inactive states are consistent across both chip groups
- β Styling follows design tokens (accent color, spacing)
- β All filter functionality accessible via keyboard
- β No keyboard traps
- β Tab key properly navigates through chips
// Main component with all state management
export function TransactionHistory()
// Sub-component for individual transaction rows
function TransactionRow()
// Constants
- TX_TYPE_LABELS: Record of type -> display label
- TX_TYPE_ICONS: Record of type -> emoji icon
- TX_TYPE_COLORS: Record of type -> hex color
- TYPE_FILTER_OPTIONS: Array of filter chip options
- DATE_FILTER_OPTIONS: Array with day counts for calculations// Filter states - each change resets pagination
const [selectedType, setSelectedType] = useState<TypeFilter>("all");
const [dateRange, setDateRange] = useState<DateFilter>("all");
const [selectedLine, setSelectedLine] = useState<string>("all");
const [selectedStatus, setSelectedStatus] = useState<string>("all");
const [searchQuery, setSearchQuery] = useState("");
// UI states
const [expandedTx, setExpandedTx] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [showExportMenu, setShowExportMenu] = useState(false);allTransactions- Combined and sorted transactions from all credit linesfilteredTransactions- Result of applying all active filterspaginatedTransactions- Sliced portion for current pagepaginatedGrouped- Transactions grouped by date (Today/Yesterday/This Week/etc)stats- Summary statistics (Total Drawn, Repaid, Interest, Debt)
Each memoization has explicit dependencies to ensure re-calculation only when needed.
All functionality is covered by vitest tests:
β renders type, date, and amount filter chips as labeled pressed toggle groups
β updates the polite result count when quick amount chips change
β shows a no-results state with a clear filters action
β applies a custom amount range from the modalRun tests:
npm run test -- src/pages/TransactionHistory.test.tsxKey classes for styling filter chips:
| Class | Purpose |
|---|---|
.th-filter-chip |
Base chip styling |
.th-filter-chip[aria-pressed="true"] |
Active chip appearance |
.th-filter-chip:focus-visible |
Keyboard focus indicator |
.th-chip-group |
Container for chip groups |
.amount-range-custom-trigger |
Custom range trigger button |
.amount-range-modal |
Custom amount dialog shell |
.th-empty-no-results |
No-results state styling |
.th-clear-filters-btn |
Clear filters action button |
.th-filter-results |
Result count announcement region |
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Mobile browsers (iOS Safari, Chrome Mobile)
All modern browsers with support for:
- CSS Grid & Flexbox
- CSS Custom Properties (CSS Variables)
- ARIA attributes
- ES2020 features (optional chaining, nullish coalescing)
- Saved Filter Presets - Save common filter combinations
- Advanced Filters - Status duration, saved combinations, and additional numeric facets
- Filter Pills - Show active filters as removable pills
- Export Filtered Results - Export only filtered transactions
- Mobile Touch Optimization - Larger touch targets, swipe gestures
- Dark Mode - High contrast filter chips in dark mode
- Localization - Multi-language filter labels and messages
- WCAG 2.1 Guidelines
- WAI-ARIA Authoring Practices - Button Pattern
- MDN - aria-pressed
- MDN - aria-live
feat: add accessible filter chips and no-results state to transactions
- Implement toggleable filter chips for transaction type (All/Draw/Repay/Fee/Interest)
- Implement date range chips with quick presets (7d/30d/90d/All)
- Add aria-pressed and role="group" for accessible toggle button groups
- Add aria-live="polite" region for result count announcements
- Implement distinct empty states: no-lines, no-transactions, no-filtered-results
- Add "Clear filters" action button in no-results state
- Filter changes reset pagination to page 1
- All changes include comprehensive inline documentation
- Update tests to verify chip functionality and empty state behavior
- All tests passing, WCAG 2.1 AA compliant