Accessibility Guide
Last Updated: 2026-02-03 | Reading Time: 15 minutes
Accessibility implementation and best practices for PasteShelf.
Table of Contents
Section titled “Table of Contents”- Accessibility Commitment
- VoiceOver Support
- Keyboard Navigation
- Visual Accessibility
- Motor Accessibility
- Testing
Accessibility Commitment
Section titled “Accessibility Commitment”Principles
Section titled “Principles”PasteShelf is committed to being accessible to all users:
- WCAG 2.1 AA Compliance: Meet or exceed guidelines
- Native Integration: Use Apple’s accessibility APIs
- Inclusive Design: Consider accessibility from the start
- Continuous Improvement: Regular accessibility audits
macOS Accessibility Features Supported
Section titled “macOS Accessibility Features Supported”| Feature | Support |
|---|---|
| VoiceOver | ✅ Full |
| Keyboard Navigation | ✅ Full |
| Reduce Motion | ✅ Supported |
| Increase Contrast | ✅ Supported |
| Reduce Transparency | ✅ Supported |
| Switch Control | ✅ Compatible |
| Voice Control | ✅ Compatible |
VoiceOver Support
Section titled “VoiceOver Support”Basic Labels
Section titled “Basic Labels”// ✅ Provide clear accessibility labelsstruct ClipboardRow: View { let item: ClipboardItem
var body: some View { HStack { contentPreview Spacer() dateLabel } .accessibilityElement(children: .combine) .accessibilityLabel(accessibilityDescription) .accessibilityHint("Double tap to copy to clipboard") }
private var accessibilityDescription: String { let type = item.contentType.localizedDescription let preview = item.preview.prefix(100) let date = item.createdDate.formatted(.relative(presentation: .named)) return "\(type): \(preview). Copied \(date)" }}Custom Actions
Section titled “Custom Actions”// ✅ Add custom accessibility actionsstruct ClipboardRow: View { let item: ClipboardItem @Binding var selectedItem: ClipboardItem?
var body: some View { rowContent .accessibilityAction(named: "Copy") { copyToClipboard(item) } .accessibilityAction(named: "Delete") { deleteItem(item) } .accessibilityAction(named: "Add to Favorites") { toggleFavorite(item) } }}Rotor Support
Section titled “Rotor Support”// ✅ Support VoiceOver rotor navigationstruct ClipboardListView: View { let items: [ClipboardItem]
var body: some View { List(items) { item in ClipboardRow(item: item) } .accessibilityRotor("Favorites") { ForEach(items.filter { $0.isFavorite }) { item in AccessibilityRotorEntry(item.preview, id: item.id) } } .accessibilityRotor("Images") { ForEach(items.filter { $0.contentType == .image }) { item in AccessibilityRotorEntry("Image from \(item.sourceApp)", id: item.id) } } }}Announcements
Section titled “Announcements”// ✅ Announce important changesclass ClipboardViewModel: ObservableObject { func copyItem(_ item: ClipboardItem) { // Perform copy pasteboard.setString(item.content, forType: .string)
// Announce to VoiceOver UIAccessibility.post( notification: .announcement, argument: "Copied to clipboard" ) }
func deleteItems(_ items: [ClipboardItem]) { // Perform delete storage.delete(items)
// Announce result let message = items.count == 1 ? "Item deleted" : "\(items.count) items deleted" UIAccessibility.post( notification: .announcement, argument: message ) }}Images and Icons
Section titled “Images and Icons”// ✅ Describe meaningful imagesImage(systemName: "star.fill") .accessibilityLabel("Favorite")
// ✅ Hide decorative imagesImage("decorative-background") .accessibilityHidden(true)
// ✅ Describe content imagesAsyncImage(url: item.thumbnailURL) { image in image .accessibilityLabel(item.imageDescription ?? "Clipboard image")} placeholder: { ProgressView() .accessibilityLabel("Loading image")}Keyboard Navigation
Section titled “Keyboard Navigation”Focus Management
Section titled “Focus Management”// ✅ Proper focus handlingstruct FloatingPanelView: View { @FocusState private var focusedField: Field?
enum Field { case search case list }
var body: some View { VStack { SearchField(text: $searchText) .focused($focusedField, equals: .search)
ClipboardList(items: items) .focused($focusedField, equals: .list) } .onAppear { focusedField = .search // Focus search on open } }}Keyboard Shortcuts
Section titled “Keyboard Shortcuts”// ✅ Support standard keyboard shortcutsstruct ContentView: View { var body: some View { NavigationView { content } .keyboardShortcut("f", modifiers: .command) // ⌘F for search .keyboardShortcut(.delete, modifiers: .command) // ⌘⌫ for delete }}
// ✅ Document keyboard shortcuts for usersstruct ShortcutsHelpView: View { let shortcuts: [(String, String)] = [ ("⌘⇧V", "Open PasteShelf panel"), ("⌘F", "Focus search"), ("⌘1-9", "Paste item 1-9"), ("↑↓", "Navigate items"), ("⏎", "Paste selected item"), ("⌘⌫", "Delete selected item"), ("⌘,", "Open preferences") ]
var body: some View { List(shortcuts, id: \.0) { shortcut in HStack { Text(shortcut.0) .font(.system(.body, design: .monospaced)) Spacer() Text(shortcut.1) } } }}Tab Navigation
Section titled “Tab Navigation”// ✅ Logical tab orderstruct SettingsView: View { var body: some View { Form { Section("General") { Toggle("Launch at Login", isOn: $launchAtLogin) .accessibilityIdentifier("launchAtLogin")
Picker("History Limit", selection: $historyLimit) { // options } .accessibilityIdentifier("historyLimit") }
Section("Shortcuts") { HotkeyField(hotkey: $globalHotkey) .accessibilityIdentifier("globalHotkey") } } // Tab order follows visual order automatically }}Visual Accessibility
Section titled “Visual Accessibility”Dynamic Type
Section titled “Dynamic Type”// ✅ Support Dynamic Typestruct ClipboardRow: View { @ScaledMetric var iconSize: CGFloat = 24
var body: some View { HStack { Image(systemName: item.icon) .frame(width: iconSize, height: iconSize)
Text(item.preview) .font(.body) // Scales with Dynamic Type .lineLimit(3) } }}
// ✅ Test with largest accessibility sizesstruct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) }}Color and Contrast
Section titled “Color and Contrast”// ✅ Use semantic colors that adaptstruct StatusBadge: View { let status: Status
var body: some View { Text(status.label) .foregroundColor(status == .error ? .red : .primary) .background( Color(status == .error ? .systemRed : .secondarySystemBackground) .opacity(0.2) ) }}
// ✅ Support high contrast modestruct ThemedView: View { @Environment(\.accessibilityContrast) var contrast
var body: some View { Rectangle() .stroke( contrast == .increased ? Color.primary : Color.secondary, lineWidth: contrast == .increased ? 2 : 1 ) }}
// ✅ Don't rely on color alonestruct StatusIndicator: View { let isActive: Bool
var body: some View { HStack { Circle() .fill(isActive ? Color.green : Color.red) .frame(width: 8, height: 8) Text(isActive ? "Active" : "Inactive") // Text backup } }}Reduce Motion
Section titled “Reduce Motion”// ✅ Respect Reduce Motion settingstruct AnimatedView: View { @Environment(\.accessibilityReduceMotion) var reduceMotion @State private var isVisible = false
var body: some View { ContentView() .opacity(isVisible ? 1 : 0) .animation( reduceMotion ? nil : .easeInOut(duration: 0.3), value: isVisible ) }}Reduce Transparency
Section titled “Reduce Transparency”// ✅ Respect Reduce Transparencystruct FloatingPanel: View { @Environment(\.accessibilityReduceTransparency) var reduceTransparency
var body: some View { content .background( reduceTransparency ? Color(.windowBackgroundColor) : Color(.windowBackgroundColor).opacity(0.9) ) }}Motor Accessibility
Section titled “Motor Accessibility”Large Touch Targets
Section titled “Large Touch Targets”// ✅ Ensure adequate tap target size (44x44 minimum)struct ActionButton: View { let action: () -> Void
var body: some View { Button(action: action) { Image(systemName: "trash") .frame(width: 44, height: 44) } .contentShape(Rectangle()) // Entire frame is tappable }}Drag and Drop Alternatives
Section titled “Drag and Drop Alternatives”// ✅ Provide keyboard alternatives to drag actionsstruct ReorderableList: View { @State var items: [ClipboardItem]
var body: some View { List { ForEach(items) { item in ItemRow(item: item) // Drag and drop for mouse users .draggable(item) } .onMove(perform: moveItems) } // Keyboard alternative: Edit mode with move actions .toolbar { EditButton() } }}Switch Control Compatibility
Section titled “Switch Control Compatibility”// ✅ Group related elementsstruct ClipboardRow: View { var body: some View { HStack { content actions } .accessibilityElement(children: .combine) // Switch Control users scan one element instead of many }}Testing
Section titled “Testing”Accessibility Inspector
Section titled “Accessibility Inspector”# Open Accessibility Inspector# Xcode → Open Developer Tool → Accessibility Inspector
# Check:# - Labels are descriptive# - Hints provide guidance# - Traits are correct# - Actions are availableVoiceOver Testing
Section titled “VoiceOver Testing”# Enable VoiceOver# System Settings → Accessibility → VoiceOver → Enable
# Test:# 1. Navigate all elements with VO+Arrow keys# 2. Verify labels make sense out of context# 3. Test custom actions with VO+Shift+M# 4. Check rotor options with VO+UAutomated Testing
Section titled “Automated Testing”import XCTest
class AccessibilityTests: XCTestCase {
func testClipboardRowAccessibility() { let item = ClipboardItem(content: "Test content") let row = ClipboardRow(item: item)
// Verify accessibility label exists XCTAssertFalse(row.accessibilityLabel.isEmpty)
// Verify hint exists XCTAssertFalse(row.accessibilityHint.isEmpty) }
func testKeyboardNavigation() { let app = XCUIApplication() app.launch()
// Tab through all interactive elements app.typeKey(.tab, modifierFlags: []) XCTAssertTrue(app.searchFields.firstMatch.hasFocus)
app.typeKey(.tab, modifierFlags: []) XCTAssertTrue(app.tables.firstMatch.hasFocus) }}Accessibility Audit
Section titled “Accessibility Audit”// ✅ Run automated accessibility auditfunc testAccessibilityAudit() throws { let app = XCUIApplication() app.launch()
try app.performAccessibilityAudit()}Accessibility Checklist
Section titled “Accessibility Checklist”VoiceOver
Section titled “VoiceOver”- All interactive elements have labels
- Labels are concise and descriptive
- Hints explain non-obvious actions
- Images have descriptions or are hidden
- Announcements for important changes
- Rotor support for large lists
Keyboard
Section titled “Keyboard”- All functions accessible via keyboard
- Logical tab order
- Focus indicators visible
- Shortcuts documented
- No keyboard traps
Visual
Section titled “Visual”- Supports Dynamic Type
- Minimum 4.5:1 contrast ratio
- Works without color
- Respects Reduce Motion
- Respects Reduce Transparency
- 44x44pt minimum touch targets
- Alternatives to drag/drop
- No time-limited interactions
- Switch Control compatible
Related Documentation
Section titled “Related Documentation”| Document | Description |
|---|---|
| Internationalization | Localized accessibility |
| Testing | Accessibility testing |
| Development Guide | Implementation |
Last updated: 2026-02-03