본문으로 건너뛰기
cccskills
무료GitHub에 공개

ios-a11y

Implement accessibility in iOS apps using Swift, UIKit, and SwiftUI. Use this skill whenever working on any iOS development task that involves: making UI elements accessible to VoiceOver or other assistive technologies, adding or reviewing accessibility labels/hints/traits/actions/values, supporting Dynamic Type or text scaling, respecting Reduce Motion or reduced transparency preferences, adapting to Dark Mode or increased contrast, building accessible forms and inputs, announcing dynamic content changes, managing focus programmatically, customizing accessibility focus order, supporting external keyboard navigation, or auditing iOS code for accessibility issues. Trigger even when the user only says "SwiftUI" or "UIKit" without mentioning "accessibility" explicitly — if they're building custom controls, modals, forms, lists, or animated views, this skill applies.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills스타 16(GitHub 16 + cccskills 0)

실행 점수 미검증

설치 방법 보기

포함된 파일(5)

  • SKILL.md16.8 KB
  • references/accessibility-apis.md11.6 KB
  • references/assistive-features.md7.5 KB
  • references/input-patterns.md10.4 KB
  • references/visual-adaptations.md12.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

iOS Accessibility

Based on Appt Docs and CVS SwiftUI Accessibility.

Core Principles

  1. Semantic over custom — Use standard UIKit/SwiftUI controls. They come with accessibility built in.
  2. Label everything that matters — Every interactive element and meaningful image needs an accessibilityLabel.
  3. Expose the right role and state — Use traits to communicate what an element is and what state it's in.
  4. Respect user preferences — Dynamic Type, Reduce Motion, Dark Mode, Bold Text, and increased contrast are signals from the user, not optional polish.
  5. Test with VoiceOver — Turn it on and use your app without looking. If you can't complete the main flows, something needs fixing.

VoiceOver: Core Attributes

Label and Hint

The label is what VoiceOver announces when the element is focused. The hint explains what happens when you activate it. Keep labels concise and hints optional — only add a hint when the outcome isn't obvious from the label.

SwiftUI:

Button("✕") { dismiss() }
    .accessibilityLabel("Close")
    .accessibilityHint("Dismisses this sheet")

UIKit:

closeButton.accessibilityLabel = "Close"
closeButton.accessibilityHint = "Dismisses this sheet"

Don't include the element's role in the label — VoiceOver announces it separately. So "Submit button" is wrong; just "Submit" is right.

For pronunciation overrides and controlling how VoiceOver speaks individual words, see accessibility-apis.md.

Traits (Role and State)

Traits tell VoiceOver what an element is and how to interact with it. UIKit calls them UIAccessibilityTraits; SwiftUI uses AccessibilityTraits.

SwiftUI:

Text("Recent Orders")
    .accessibilityAddTraits(.isHeader)

Toggle("Dark mode", isOn: $isDarkMode)
// Toggle already has the correct traits

Text("Step 1 of 3")
    .accessibilityAddTraits(.updatesFrequently)

UIKit:

sectionLabel.accessibilityTraits = .header
linkButton.accessibilityTraits = .link
toggleSwitch.accessibilityTraits = [.button, .selected] // when on

disabledButton.accessibilityTraits.remove(.button)
disabledButton.accessibilityTraits.insert(.notEnabled)

Common traits: .button .link .header .image .adjustable .selected .notEnabled .staticText .searchField

For the .adjustable trait (custom sliders/steppers with increment/decrement actions), accessibilityRepresentation for fully custom controls, and accessibilityRespondsToUserInteraction, see accessibility-apis.md.

Value

Use accessibilityValue to describe the current state of adjustable or interactive elements — sliders, steppers, toggles, or progress indicators.

SwiftUI:

Slider(value: $volume, in: 0...1)
    .accessibilityLabel("Volume")
    .accessibilityValue("\(Int(volume * 100)) percent")

UIKit:

volumeSlider.accessibilityLabel = "Volume"
volumeSlider.accessibilityValue = "\(Int(volumeSlider.value * 100)) percent"

Hiding Elements

Decorative images, visual dividers, and purely presentational elements should be hidden from assistive technologies.

SwiftUI:

Image("decorative-background").accessibilityHidden(true)
// For icons paired with a label, hide the icon: Image(...).accessibilityHidden(true)

UIKit:

decorativeImageView.isAccessibilityElement = false
separatorView.isAccessibilityElement = false

Grouping and Order

Combining Elements

When multiple views together form one logical unit, combine them so VoiceOver reads them as a single item.

SwiftUI:

HStack {
    Image(systemName: "star.fill")
        .accessibilityHidden(true)
    VStack(alignment: .leading) {
        Text("Highly Rated")
        Text("4.8 out of 5")
    }
}
.accessibilityElement(children: .combine)
// VoiceOver reads: "Highly Rated, 4.8 out of 5"

UIKit:

containerView.isAccessibilityElement = true
containerView.accessibilityLabel = "Highly Rated, 4.8 out of 5"
imageView.isAccessibilityElement = false
titleLabel.isAccessibilityElement = false
subtitleLabel.isAccessibilityElement = false

Gotcha when combining: if a child Button is combined with .combine, its label won't transfer. Either remove .isButton from the child first, or set .accessibilityLabel explicitly on the combined parent.

Grouping Controls

Use .contain (not .combine) when you want a group label announced on entry but children to stay individually focusable — the right pattern for form sections, radio groups, and card regions.

SwiftUI:

VStack {
    Text("Shipping address")
    TextField("Street", text: $street)
    TextField("City", text: $city)
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Shipping address")
// VoiceOver announces "Shipping address, group" on entry, then reads each field individually

Warning: adding .accessibilityLabel to a container without .accessibilityElement(children: .contain) silently overrides every child element's label — this breaks Voice Control's "Tap [name]" command.

Custom Ordering

When the default left-to-right, top-to-bottom focus order doesn't match logical reading order, override it.

SwiftUI:

VStack {
    Text("$29.99")
        .accessibilitySortPriority(2)   // focused first
    Text("Price")
        .accessibilitySortPriority(1)
}

UIKit: Set accessibilityElements on the container to define explicit order:

containerView.accessibilityElements = [titleLabel, priceLabel, addToCartButton]

Focus Management

Announcements

Post announcements to notify users of assistive technologies about important, non-visual changes — a form submitted, an item added to cart, an error appearing.

SwiftUI / UIKit (both):

// iOS 15+
AccessibilityNotification.Announcement("Item added to cart").post()

// All iOS versions
UIAccessibility.post(notification: .announcement, argument: "Item added to cart")

When posting from a state change, add a short delay so VoiceOver doesn't skip the announcement:

.onChange(of: itemAdded) { _ in
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        AccessibilityNotification.Announcement("Item added to cart").post()
    }
}

For live regions (auto-announcing in-place content updates) and the full notification reference, see accessibility-apis.md.

Moving Focus

When presenting a new view (modal, bottom sheet, inline expansion), move VoiceOver focus to the right element so the user knows something changed.

SwiftUI:

@AccessibilityFocusState private var confirmFocused: Bool

VStack {
    if showConfirmation {
        Text("Order confirmed!")
            .accessibilityFocused($confirmFocused)
    }
}
.onChange(of: showConfirmation) { newValue in
    if newValue { confirmFocused = true }
}

UIKit:

UIAccessibility.post(notification: .screenChanged, argument: newViewController.view)
UIAccessibility.post(notification: .layoutChanged, argument: specificView)

Use .screenChanged when the whole screen context changes; .layoutChanged for in-place updates.

Modals

Mark custom overlays so VoiceOver can't wander into background content. SwiftUI sheets and .fullScreenCover handle this automatically.

// SwiftUI custom overlay — also handle two-finger scrub to dismiss
VStack { /* modal content */ }
    .accessibilityAddTraits(.isModal)
    .accessibilityAction(.escape) { isPresented = false }

// UIKit
modalContainerView.accessibilityViewIsModal = true
UIAccessibility.post(notification: .screenChanged, argument: modalContainerView)

For the escape gesture protocol, custom rotor entries, and focus indicators for Keyboard Access, see accessibility-apis.md.

Keyboard Dismiss Focus

Text fields don't return VoiceOver focus after keyboard dismissal. Use @AccessibilityFocusState to send it back explicitly:

@AccessibilityFocusState private var fieldFocused: Bool

TextField("Name", text: $name)
    .accessibilityFocused($fieldFocused)
    .toolbar {
        ToolbarItem(placement: .keyboard) {
            Button("Done") {
                dismissKeyboard()
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    fieldFocused = true
                }
            }
        }
    }

Custom Actions

When an element supports multiple actions (swipe to delete, long-press to share, drag to reorder), expose them as accessibility actions so Switch Control and VoiceOver users can access them without the gesture.

SwiftUI:

Text(item.name)
    .accessibilityAction(named: "Delete") { deleteItem(item) }
    .accessibilityAction(named: "Share") { shareItem(item) }
    .accessibilityAction(named: "Move up") { moveItemUp(item) }

UIKit:

cell.accessibilityCustomActions = [
    UIAccessibilityCustomAction(name: "Delete") { [weak self] _ in
        self?.deleteItem(item); return true
    }
]

For drag-and-drop accessibility and Switch Control scanning behavior, see assistive-features.md.


Magic Tap

Magic Tap (two-finger double-tap anywhere on screen) toggles the app's primary action — play/pause, answer/end a call. Define at most one per screen. See accessibility-apis.md for the implementation.


Visual Adaptations

Dynamic Type

Text that doesn't scale with the user's preferred font size is one of the most common iOS accessibility failures. Always use text styles; never hard-code font sizes.

SwiftUI:

Text("Hello world")
    .font(.headline)        // respects user's size preference

@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24
Image(systemName: "star")
    .frame(width: iconSize, height: iconSize)

UIKit:

label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
// Never: UIFont.systemFont(ofSize: 16)

Never set a fixed height on a view that contains text. Use .numberOfLines = 0 on UILabel.

For text spacing overrides, preventing truncation at accessibility sizes, reflow layouts, and localization, see visual-adaptations.md.

Reduce Motion

Users with vestibular disorders may have Reduce Motion enabled. Check this before playing animations.

SwiftUI:

@Environment(\.accessibilityReduceMotion) var reduceMotion

Circle()
    .scaleEffect(isAnimating ? 1.2 : 1.0)
    .animation(reduceMotion ? nil : .easeInOut(duration: 0.6), value: isAnimating)

UIKit:

if UIAccessibility.isReduceMotionEnabled {
    view.alpha = isVisible ? 1 : 0
} else {
    UIView.animate(withDuration: 0.4) { view.alpha = isVisible ? 1 : 0 }
}

For Large Content Viewer, Reduce Transparency, Dark Mode, Increased Contrast, Bold Text, Smart Invert, Dim Flashing Lights, and audio/media accessibility, see visual-adaptations.md.


Input Accessibility

Labels for Input Fields

Every form field needs a visible label and an accessibilityLabel. A placeholder alone is not sufficient — it disappears when the user starts typing.

SwiftUI:

VStack(alignment: .leading) {
    Text("Email address")
        .font(.caption)
    TextField("Email address", text: $email)
        .keyboardType(.emailAddress)
        .textContentType(.emailAddress)
        .accessibilityLabel("Email address")
}

UIKit:

emailTextField.placeholder = "Email address"
emailTextField.accessibilityLabel = "Email address"
emailTextField.keyboardType = .emailAddress
emailTextField.textContentType = .emailAddress

Voice Control Labels

Voice Control users activate elements by speaking their visible label. When a label is ambiguous or not naturally speakable, use accessibilityInputLabels to provide alternatives.

Button("→") { nextPage() }
    .accessibilityLabel("Next page")
    .accessibilityInputLabels(["Next", "Next page", "Forward"])

For how Voice Control's "Show Names" and "Show Numbers" modes work, and how to verify your UI, see assistive-features.md.

Error Handling

Show the error message visually and announce it so VoiceOver users know something is wrong.

SwiftUI:

VStack(alignment: .leading) {
    TextField("Email", text: $email)
        .accessibilityLabel("Email")
    if let error = emailError {
        Text(error).foregroundStyle(.red).font(.caption)
    }
}
.onChange(of: emailError) { newError in
    if let error = newError {
        AccessibilityNotification.Announcement(error).post()
    }
}

UIKit:

func showError(_ message: String) {
    errorLabel.text = message
    errorLabel.isHidden = false
    UIAccessibility.post(notification: .announcement, argument: message)
}

For keyboard type and content type, tap target sizing, timing adjustments, and accessible authentication, see input-patterns.md.


Screen Structure

Screen Title

Every screen needs a title — it orients all users, especially VoiceOver users navigating to a new screen.

SwiftUI: NavigationStack { ContentView().navigationTitle("Order History") }

UIKit: navigationItem.title = "Order History"

Section Headers

Mark section headers so VoiceOver users can jump between sections with the rotor.

SwiftUI: Text("Recent").font(.headline).accessibilityAddTraits(.isHeader)

UIKit: sectionLabel.accessibilityTraits = .header

For screen orientation support, see visual-adaptations.md.


Checking AT State at Runtime

UIAccessibility.isVoiceOverRunning
UIAccessibility.isSwitchControlRunning
UIAccessibility.isReduceMotionEnabled
UIAccessibility.isDarkerSystemColorsEnabled
UIAccessibility.isBoldTextEnabled
UIAccessibility.isGrayscaleEnabled
UIAccessibility.preferredContentSizeCategory  // current Dynamic Type size

For subscribing to runtime change notifications, see accessibility-apis.md.


Testing

Manual Testing

  • VoiceOver: Navigate core flows without looking at the screen. Every element should have a meaningful label, correct role, and logical focus order.
  • Accessibility Inspector: Xcode → Open Developer Tool → Accessibility Inspector. Run audits for missing labels, small targets, and contrast issues.
  • Dynamic Type: Test at the largest accessibility size; verify nothing clips or truncates.
  • Voice Control: Settings → Accessibility → Voice Control. Use "Show Names" to verify every interactive element has a unique, speakable label.

For a full testing checklist and how to enable/use VoiceOver, Switch Control, Voice Control, and Keyboard Access, see assistive-features.md.

Automated Testing with XCTest

performAccessibilityAudit() (iOS 17+) catches missing labels, small tap targets, contrast failures, and text clipping:

func testMyScreen() throws {
    let app = XCUIApplication()
    app.launch()
    navigateToMyScreen(app)
    try app.performAccessibilityAudit()
    app.swipeUp()  // audit below-fold content too
    try app.performAccessibilityAudit()
}

Filter known false positives:

try app.performAccessibilityAudit { issue in
    issue.auditType == .contrast
}

Write manual assertions for what the audit misses (duplicate labels, redundant role words, heading traits):

// XCTest IDs go in .accessibilityIdentifier() — VoiceOver never reads them
XCTAssertFalse(app.buttons["closeButton"].label.isEmpty)
XCTAssertFalse(app.buttons["closeButton"].label.lowercased().contains("button"))
XCTAssertNotEqual(app.buttons["edit1"].label, app.buttons["edit2"].label)

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Style and fully customize the Brightcove video player UI — control bar, play button, progress bar, volume, captions, playlists, responsive sizing, and skins. Use this skill whenever the user mentions Brightcove, video-js player styling, customizing a Brightcove player, changing player colors/layout/controls, embedding a Brightcove player, making it responsive, player skins or themes, Brightcove Studio styling, or working with Brightcove playlists or captions. Also use it when the user is working with `.video-js`, `vjs-*` CSS classes, or `bc()` / `videojs.getPlayer()` / `videojs()` JavaScript APIs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

Structure and organize Storybook files for scalability using battle-tested patterns. Based on "A Storybook format that scales with you" by Cassondra Roberts. Always use this skill when creating or editing any Storybook story file, writing template files, organizing a component library, setting up visual regression tests with Chromatic, or when the user asks anything about Storybook — even casual questions about file structure, controls, args, or how to document a component.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

Write minimal, accessible web UI without over-engineering, adapting to whatever stack the project uses — React, Vue, Svelte, Angular, web components, or plain HTML. Use this skill on every project and every task that writes or reviews markup or components, builds forms, adds interactive elements like buttons, dialogs, accordions, or tabs, or audits code for accessibility — even when the user never mentions accessibility, and even inside a framework, CMS, or design system. It's non-negotiable regardless of project type, stack, or deadline. About to reach for ARIA attributes, a dialog library, a focus-trap package, or a headless UI component? Use this skill first.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

Define and enforce consistent coding standards across HTML, CSS, and JavaScript. Always use this skill when naming a new class, variable, component, or file; setting up a new project's conventions; choosing a class prefix for a new CSS category; deciding on modifier API names (sizes, shades, hierarchy, breakpoints); or reviewing code for formatting and naming consistency. If you're about to invent a prefix, abbreviation, or modifier name without checking the conventions first, use this skill.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

Create web interfaces with an authentic early-2010s aesthetic. Use this skill when the user wants a 2010s-era, Web 2.0, or retro corporate web look — gradient headers, glossy buttons, skeuomorphic icons, horizontal band layouts, and drop shadows from circa 2010–2014.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

more-css

무료

Write scalable, well-architected CSS using design tokens, cascade layers, and modern organization patterns. Use this skill as the default for any real project — if it has more than a handful of CSS files, multiple components, a team, a design system, or any kind of token or theming setup, this is the right skill.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

mikemai2awesome/agent-skills162026년 7월 31일 업데이트

mikemai2awesome의 스킬 모두 보기

이 스킬의 문제 신고