一键导入
accessibility
Build WCAG 2.2 AA compliant interfaces — focus management, ARIA utilities, motion preferences, color contrast, screen reader support
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build WCAG 2.2 AA compliant interfaces — focus management, ARIA utilities, motion preferences, color contrast, screen reader support
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Run the asset_pipeline convention linter (`scripts/lint_conventions.cr`) — Crystal-side runner that enforces Phase 10 Family 1 naming rules across src/, samples/, and spec/. Use before commit, in CI, or when reviewing a PR.
Build cross-platform apps with UI::App + UI::Screen + UI::Controller + UI::ActionDispatcher + UI::FormState — route declarations, native action dispatch, Amber web integration, and the static-site web target.
The developer-facing guide to building Apple UI (iOS 26, iPadOS 26, macOS 26) with asset_pipeline's UI::View system. Backed by the Apple HIG corpus and validated by macOS + iOS screenshots. Answers "I want to show X — what component, what args, what HIG says."
Complete catalog of Android Jetpack Compose and Material Design 3 components. Covers M3 Expressive (2025), gesture system, graphics/drawing, navigation patterns, theming, and mapping guidance for Crystal cross-platform UI integration via JNI.
Full Apple Human Interface Guidelines corpus packaged offline as searchable markdown. Covers iOS, iPadOS, and macOS guidance for every HIG page, with cross-referenced links, downloaded illustrations, and a tag index. Use as the authoritative design reference when building native UI with asset_pipeline.
Comprehensive cross-platform UI component mapping matrix showing how every common UI pattern maps across Crystal UI::View, SwiftUI, UIKit, AppKit, Jetpack Compose, Android Views, and Web HTML/CSS. Implementation priority guide for expanding the asset_pipeline cross-platform component system.
| name | accessibility |
| description | Build WCAG 2.2 AA compliant interfaces — focus management, ARIA utilities, motion preferences, color contrast, screen reader support |
| user-invocable | true |
You are an accessibility specialist who ensures all components meet WCAG 2.2 Level AA. The asset pipeline has accessibility built into its defaults — focus rings, motion preferences, color contrast, and ARIA state utilities are all provided out of the box.
The CSS generator automatically includes these in the @layer base block:
All interactive elements get a visible focus indicator on keyboard navigation:
:focus-visible {
outline: 2px solid var(--color-blue-500);
outline-offset: 2px;
}
No additional code needed. Every <button>, <a>, <input>, etc. will show a focus ring when tabbed to.
Animations are disabled when users prefer reduced motion:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Focus indicators work in Windows High Contrast mode:
@media (forced-colors: active) {
:focus-visible {
outline: 2px solid CanvasText;
}
}
:root { color-scheme: light dark; }
Tells the browser to adapt form controls, scrollbars, and system colors to the user's preference.
# Visually hidden but available to screen readers
span = Components::Elements::Span.new(class: "sr-only")
span << "Close dialog"
# Undo sr-only (e.g., on focus)
link = Components::Elements::A.new(href: "#main", class: "sr-only focus:not-sr-only")
link << "Skip to main content"
sr-only generates:
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0;
}
ring Default focus ring (3px)
ring-0 No ring
ring-1 through ring-8 Ring width 1-8px
ring-inset Inset ring
outline-0 through outline-4 Outline width
outline-offset-0 through outline-offset-4 Outline offset
Example: focus-visible:ring-2 focus-visible:ring-blue-500
Use min-width/min-height utilities to ensure touch targets are at least 44x44px:
button = Components::Elements::Button.new("Tap me", type: "button")
button.add_class("min-w-11 min-h-11") # 2.75rem = 44px
The ClassBuilder has dedicated methods for accessibility-related states:
classes = Components::CSS::ClassBuilder.new
.base("btn", "btn-primary")
.focus_visible("ring-2", "ring-blue-500", "outline-none")
.build
Generates: focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none
CSS output: .focus-visible\:ring-2:focus-visible { box-shadow: 0 0 0 2px ...; }
classes = Components::CSS::ClassBuilder.new
.base("transition-transform")
.motion_safe("hover:scale-105") # Only animate if user allows motion
.motion_reduce("hover:shadow-lg") # Alternative non-motion effect
.build
CSS output wraps in @media (prefers-reduced-motion: no-preference) and @media (prefers-reduced-motion: reduce).
Style elements based on their ARIA state — no JavaScript needed for visual changes:
# Accordion item
classes = Components::CSS::ClassBuilder.new
.base("accordion-trigger", "p-4", "border-b")
.aria_expanded("bg-blue-50", "font-semibold") # When expanded
.build
# Tab
classes = Components::CSS::ClassBuilder.new
.base("tab", "px-4", "py-2")
.aria_selected("border-b-2", "border-blue-500", "text-blue-600")
.build
# Checkbox label
classes = Components::CSS::ClassBuilder.new
.base("checkbox-label")
.aria_checked("text-green-600")
.build
# Disabled element
classes = Components::CSS::ClassBuilder.new
.base("btn")
.aria_disabled("opacity-50", "cursor-not-allowed")
.build
CSS output uses attribute selectors:
.aria-expanded\:bg-blue-50[aria-expanded="true"] { background-color: ...; }
.aria-selected\:border-b-2[aria-selected="true"] { border-bottom-width: 2px; }
classes = Components::CSS::ClassBuilder.new
.base("input", "border", "rounded")
.invalid("border-red-500", "text-red-900")
.valid("border-green-500")
.build
Beyond ClassBuilder methods, these modifiers work with any utility class:
| Prefix | CSS Selector | WCAG Criterion |
|---|---|---|
focus-visible: | :focus-visible | 2.4.7 Focus Visible |
focus-within: | :focus-within | 2.4.7 (container) |
motion-safe: | @media (prefers-reduced-motion: no-preference) | 2.2.2, 2.3.1 |
motion-reduce: | @media (prefers-reduced-motion: reduce) | 2.2.2, 2.3.1 |
contrast-more: | @media (prefers-contrast: more) | 1.4.6 Enhanced Contrast |
forced-colors: | @media (forced-colors: active) | 1.4.1 Use of Color |
aria-expanded: | [aria-expanded="true"] | 4.1.2 Name, Role, Value |
aria-selected: | [aria-selected="true"] | 4.1.2 |
aria-checked: | [aria-checked="true"] | 4.1.2 |
aria-disabled: | [aria-disabled="true"] | 4.1.2 |
aria-hidden: | [aria-hidden="true"] | 4.1.2 |
aria-pressed: | [aria-pressed="true"] | 4.1.2 |
aria-busy: | [aria-busy="true"] | 4.1.2 |
required: | :required | 3.3.2 Labels or Instructions |
invalid: | :invalid | 3.3.1 Error Identification |
valid: | :valid | 3.3.1 |
checked: | :checked | 4.1.2 |
indeterminate: | :indeterminate | 4.1.2 |
read-only: | :read-only | 3.3.2 |
placeholder-shown: | :placeholder-shown | 3.3.2 |
pointer-coarse: | @media (pointer: coarse) | 2.5.5 Target Size |
pointer-fine: | @media (pointer: fine) | 2.5.5 |
inert: | :is([inert], [inert] *) | 2.4.7 |
open: | :is([open], details[open]) | 4.1.2 |
Element classes enforce accessibility-relevant attributes at construction time:
# Th validates scope attribute
th = Components::Elements::Th.new(scope: "col") # Valid: row, col, rowgroup, colgroup
th << "Name"
# Invalid scope raises ArgumentError
Components::Elements::Th.new(scope: "invalid") # => ArgumentError
# Label with for attribute
label = Components::Elements::Label.new("Email", for: "email-input")
input = Components::Elements::Input.email("email")
input.set_attribute("id", "email-input")
input.set_attribute("aria-describedby", "email-help")
help = Components::Elements::Span.new(id: "email-help", class: "sr-only")
help << "Enter your email address"
# Img validates loading and decoding
img = Components::Elements::Img.new(
src: "/photo.jpg",
alt: "A sunset over mountains", # Always provide alt text
loading: "lazy", # Valid: lazy, eager
decoding: "async" # Valid: sync, async, auto
)
# Decorative images
img = Components::Elements::Img.new(src: "/decoration.svg", alt: "", role: "presentation")
# A validates target and rel
link = Components::Elements::A.new(
href: "https://example.com",
target: "_blank",
rel: "noopener noreferrer" # Security + accessibility for external links
)
# Form validates method and enctype
form = Components::Elements::Form.new(method: "POST", action: "/submit")
# Input validates type-specific attributes
input = Components::Elements::Input.number("quantity")
input.set_attribute("min", "1")
input.set_attribute("max", "99")
input.set_attribute("required", "true")
input.set_attribute("aria-label", "Quantity")
skip = Components::Elements::A.new(href: "#main-content", class: "sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:p-4 focus:bg-white focus:rounded")
skip << "Skip to main content"
# ... header/nav ...
main = Components::Elements::Main.new(id: "main-content")
# Use semantic elements — they map to ARIA landmarks automatically
Components::Elements::Header.new # => banner
Components::Elements::Nav.new # => navigation
Components::Elements::Main.new # => main
Components::Elements::Aside.new # => complementary
Components::Elements::Footer.new # => contentinfo
# Add aria-label to distinguish multiple navs
Components::Elements::Nav.new(class: "breadcrumb").tap { |n| n.set_attribute("aria-label", "Breadcrumb") }
Components::Elements::Nav.new(class: "main-nav").tap { |n| n.set_attribute("aria-label", "Main navigation") }
# Uses Details/Summary — native keyboard accessible
details = Components::Elements::Details.new(class: "accordion-item")
summary = Components::Elements::Summary.new(class: "accordion-trigger p-4 cursor-pointer")
summary << "Section Title"
details << summary
content = Components::Elements::Div.new(class: "accordion-content p-4")
content << "Section content here."
details << content
Style with ARIA/open states:
trigger_classes = Components::CSS::ClassBuilder.new
.base("accordion-trigger", "p-4", "cursor-pointer")
.hover("bg-gray-50")
.focus_visible("ring-2", "ring-blue-500")
.build
dialog = Components::Elements::Dialog.new(class: "modal", id: "confirm-dialog")
dialog.set_attribute("aria-labelledby", "dialog-title")
dialog.set_attribute("aria-describedby", "dialog-desc")
dialog.build do |d|
h2 = Components::Elements::H2.new(id: "dialog-title")
h2 << "Confirm Action"
d << h2
p = Components::Elements::P.new(id: "dialog-desc")
p << "Are you sure you want to proceed?"
d << p
actions = Components::Elements::Div.new(class: "flex gap-4 justify-end")
cancel = Components::Elements::Button.new("Cancel", type: "button")
cancel.set_attribute("data-action", "click->dialog#close")
confirm = Components::Elements::Button.new("Confirm", type: "button")
confirm.add_class("btn-primary")
actions.add_children(cancel, confirm)
d << actions
end
table = Components::Elements::Table.new
table.set_attribute("aria-label", "User accounts")
# Caption for context
caption = Components::Elements::Caption.new
caption << "Active user accounts as of February 2026"
table << caption
# Headers with scope
thead = Components::Elements::Thead.new
tr = Components::Elements::Tr.new
["Name", "Email", "Role", "Status"].each do |col|
th = Components::Elements::Th.new(scope: "col")
th << col
tr << th
end
thead << tr
table << thead
# Announce updates to screen readers
status = Components::Elements::Div.new(
class: "sr-only",
role: "status"
)
status.set_attribute("aria-live", "polite")
status.set_attribute("aria-atomic", "true")
status << "3 new messages"
# Associate errors with inputs via aria-describedby
input = Components::Elements::Input.email("email")
input.set_attribute("id", "email")
input.set_attribute("aria-describedby", "email-error")
input.set_attribute("aria-invalid", "true")
input.add_class("border-red-500")
error = Components::Elements::Span.new(id: "email-error", class: "text-red-600 text-sm", role: "alert")
error << "Please enter a valid email address."
When building components, verify:
alt text (or alt="" + role="presentation" for decorative):focus-visible default)min-w-11 min-h-11)aria-invalid + aria-describedbyfor, or aria-label)aria-expanded:, aria-selected:, etc.)