From unleashed-mail
Accessibility patterns and compliance for UnleashedMail. Covers VoiceOver support, keyboard navigation, Dynamic Type, color contrast, and testing. Activates when implementing accessible UI, testing a11y features, or ensuring compliance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/unleashed-mail:accessibility-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
UnleashedMail is fully accessible with VoiceOver, keyboard navigation, Dynamic Type,
UnleashedMail is fully accessible with VoiceOver, keyboard navigation, Dynamic Type, and color contrast compliance. All UI components follow macOS accessibility guidelines. Accessibility is mandatory — no feature ships without full a11y support.
Button("Send", systemImage: "paperplane") {
// action
}
.accessibilityLabel("Send email")
.accessibilityHint("Sends the composed email to recipients")
// Custom gesture-based control
Rectangle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.onTapGesture {
toggleStar()
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(isStarred ? "Remove star" : "Add star")
.accessibilityHint("Toggles star status for this message")
.accessibilityAddTraits(.isButton)
.accessibilityValue(isStarred ? "Starred" : "Not starred")
List(messages, selection: $selectedMessage) { message in
MessageRow(message: message)
}
.accessibilityLabel("Message list")
.accessibilityHint("Select a message to view its contents")
struct MessageRow: View {
let message: MailMessage
var body: some View {
HStack {
Circle()
.fill(message.isRead ? Color.clear : Color.blue)
.frame(width: 8, height: 8)
.accessibilityHidden(true) // Decorative
VStack(alignment: .leading) {
Text(message.sender)
.font(.headline)
Text(message.subject)
.font(.subheadline)
.lineLimit(1)
Text(message.snippet)
.font(.caption)
.lineLimit(1)
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel(accessibilityLabel)
.accessibilityHint("Double tap to open message")
.accessibilityValue(message.isRead ? "Read" : "Unread")
}
private var accessibilityLabel: String {
let readStatus = message.isRead ? "" : "Unread, "
return "\(readStatus)From \(message.sender), \(message.subject)"
}
}
@AccessibilityFocusState private var focusedField: Field?
enum Field {
case to, subject, body
}
TextField("To", text: $to)
.accessibilityFocused($focusedField, equals: .to)
TextField("Subject", text: $subject)
.accessibilityFocused($focusedField, equals: .subject)
TextEditor(text: $body)
.accessibilityFocused($focusedField, equals: .body)
// Custom rotor for navigation
.accessibilityRotor("Unread Messages") {
ForEach(unreadMessages) { message in
AccessibilityRotorEntry(message.subject, id: message.id)
}
}
Text("Welcome to UnleashedMail")
.font(.largeTitle) // Scales with Dynamic Type
// Custom font that scales
Text("Message")
.font(.system(size: 16, weight: .medium, design: .default)) // ❌ Doesn't scale
Text("Message")
.font(.body) // ✅ Scales automatically
VStack {
Text("Subject")
.font(.headline)
Text(subject)
.font(.body)
.lineLimit(nil) // Allow wrapping
.fixedSize(horizontal: false, vertical: true) // Grow vertically
}
// ✅ Adapts to light/dark mode and high contrast
Color.primary
Color.secondary
Color.accentColor
// ❌ Hardcoded colors
Color.blue
Color.white
// ✅ Multiple indicators
HStack {
Image(systemName: message.isStarred ? "star.fill" : "star")
Text(message.subject)
}
.foregroundStyle(message.isStarred ? .yellow : .gray)
// ❌ Color only
Text(message.subject)
.foregroundStyle(message.isStarred ? .yellow : .primary)
// Announce when content changes
.onChange(of: messageCount) { oldValue, newValue in
let announcement = "\(newValue) messages"
AccessibilityNotification.Announcement(announcement).post()
}
func sendEmail() async {
// Send logic...
AccessibilityNotification.Announcement("Email sent successfully").post()
}
# Launch Accessibility Inspector
open /Applications/Xcode.app/Contents/Developer/Applications/Accessibility\ Inspector.app
func testMessageRow_accessibility() throws {
let app = XCUIApplication()
app.launch()
let messageRow = app.descendants(matching: .any)["Message from John Doe"]
XCTAssertTrue(messageRow.exists)
XCTAssertEqual(messageRow.label, "From John Doe, Welcome to the team")
XCTAssertEqual(messageRow.value as? String, "Unread")
}
TextField("Email Address", text: $email)
.accessibilityLabel("Recipient email address")
.accessibilityHint("Enter the email address of the recipient")
.textContentType(.username)
ProgressView("Sending email...", value: progress)
.accessibilityLabel("Sending email progress")
.accessibilityValue("\(Int(progress * 100)) percent complete")
.sheet(isPresented: $showCompose) {
ComposeView()
.accessibilityAddTraits(.isModal)
}
Since UnleashedMail has dual implementations (native + WebKit compose, simple + full email detail), both must be equally accessible:
// Native SwiftUI editor
TextEditor(text: $body)
.accessibilityLabel("Email body")
.accessibilityHint("Compose the content of your email")
// WebKit editor
WebView(html: composeHTML)
.accessibilityLabel("Email composition editor")
.accessibilityHint("Use rich text editing to compose your email")
// Simple WebView
WebView(html: messageHTML)
.accessibilityLabel("Email content")
.accessibilityHint("Read the full content of the email")
// Full WebView (with additional features)
WebView(html: enhancedHTML)
.accessibilityLabel("Email content with attachments")
.accessibilityHint("Read the email and access attachments")
All features must pass these standards before release.
npx claudepluginhub npranson/unleashed-mail-plugin --plugin unleashed-mailGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.