Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.prism.byescaleira.com/llms.txt

Use this file to discover all available pages before exploring further.

PrismUI treats accessibility as a first-class requirement, not an afterthought. Every built-in component ships with accessibility labels, respects Dynamic Type, and checks accessibilityReduceMotion before running any animation. Beyond the components themselves, PrismUI provides a set of utilities that let you audit, test, and validate accessibility in your own code.

VoiceOver support

Every PrismUI component sets appropriate accessibility labels, values, hints, and traits without any configuration on your part. Here are a few examples of what happens automatically:
  • PrismButton inherits its label from the button title and announces loading state changes.
  • PrismAvatar collapses its sub-views into a single element with a label that combines initials and status (for example, “JD, online”).
  • PrismRating announces the current value as a fraction: “3 out of 5 stars”.
  • PrismPinField announces entry progress: “2 of 6 entered”.
  • PrismProgressBar announces the percentage for determinate progress and “In progress” for indeterminate.
For your own components, use the prismAccessibility() modifier to set label, hint, value, and traits in one call:
Image(systemName: "heart.fill")
    .prismAccessibility(
        label: Text("Add to favorites"),
        hint: Text("Double-tap to toggle"),
        traits: .isButton
    )
Mark section headings so VoiceOver users can jump between sections by heading:
Text("Recent transactions")
    .prismFont(.headline)
    .prismAccessibilityHeader()
Group related sub-views into a single accessible element:
HStack {
    Image(systemName: "arrow.up")
    Text("+$124.00")
}
.prismAccessibilityGroup(label: Text("Gain of 124 dollars"))

Dynamic Type

All typography in PrismUI uses TypographyToken, which maps to Font.TextStyle values. SwiftUI scales these automatically with the user’s preferred text size — from .xSmall through .accessibility5 — without any extra configuration. Use PrismDynamicTypePreview in your SwiftUI Previews to verify that your components look correct across the full Dynamic Type range before you ship:
#Preview {
    PrismDynamicTypePreview {
        TransactionRow(transaction: .sample)
    }
}
This renders your component at five representative sizes: .xSmall, .medium, .large, .xxxLarge, and .accessibility3, stacked vertically in a scroll view.
Never use fixed CGFloat font sizes in components that will render user content. Always use TypographyToken or a Font.TextStyle so Dynamic Type works correctly.

Reduce Motion

PrismUI reads @Environment(\.accessibilityReduceMotion) in every component that animates. When the user enables Reduce Motion in system settings, all animations are disabled automatically throughout the component library.

MotionToken.instant vs MotionToken.expressive

MotionToken.instant uses a 0.10 s linear curve — appropriate for state changes that should feel immediate and non-distracting. MotionToken.expressive uses a 0.50 s spring with a 0.2 bounce — appropriate for delight-driven transitions like expanding cards or onboarding sequences. When Reduce Motion is on, both tokens produce no animation. When it is off, instant provides a barely perceptible transition while expressive delivers a visible spring effect:
// Fast, subtle — good for checkbox state or loading indicator
icon
    .prismAnimation(.instant, value: isChecked)

// Springy, engaging — good for card expand or modal entrance
card
    .prismAnimation(.expressive, value: isExpanded)

Conditional motion in custom views

Use .prismMotionSafe() to apply a transform only when Reduce Motion is disabled:
Circle()
    .prismMotionSafe { view in
        view.rotationEffect(.degrees(isLoading ? 360 : 0))
            .animation(.linear(duration: 1).repeatForever(autoreverses: false),
                       value: isLoading)
    }
Use PrismReduceMotion when you need to show different views entirely for reduced and full motion:
PrismReduceMotion {
    // Shown when Reduce Motion is on
    Text("Loading…")
        .prismFont(.caption)
} full: {
    // Shown when Reduce Motion is off
    LottieAnimationView(name: "spinner")
}

WCAG contrast validation

PrismContrastChecker calculates WCAG 2.x relative luminance and contrast ratios. Use it to verify that any foreground/background combination in your app meets a target compliance level.

Compliance levels

LevelUse caseMinimum ratio
.aaNormal text (body, labels)4.5:1
.aaaEnhanced normal text7.0:1
.aaLargeTextLarge text (18 pt+, or 14 pt bold+)3.0:1
.aaaLargeTextEnhanced large text4.5:1

Checking a color pair

let ratio = PrismContrastChecker.contrastRatio(
    between: theme.color(.onBackground),
    and: theme.color(.background)
)
print("Contrast ratio: \(ratio)")  // e.g. 7.84

let passes = PrismContrastChecker.meetsLevel(
    .aa,
    foreground: theme.color(.onBackground),
    background: theme.color(.background)
)

Suggesting an accessible alternative

If a color pair fails a target level, suggestAccessibleColor adjusts the foreground’s lightness until the ratio is met:
let accessibleForeground = PrismContrastChecker.suggestAccessibleColor(
    for: brandTint,
    on: theme.color(.background),
    level: .aa
)

HighContrastTheme

HighContrastTheme maximizes contrast ratios beyond WCAG AAA. It uses pure platform foreground and background colors for text and surfaces, bolder border opacities, and saturated feedback colors:
ContentView()
    .prismTheme(HighContrastTheme())
To let users opt in, expose a toggle in your settings screen and persist the preference:
@State private var useHighContrast = false

PrismToggle("High contrast", isOn: $useHighContrast)
    .onChange(of: useHighContrast) { _, enabled in
        themeStore.setTheme(enabled ? "highContrast" : "default")
    }
PrismThemeStore includes HighContrastTheme registered as "highContrast" by default, so no extra registration is needed.

Color blindness simulation

PrismColorBlindnessSimulator applies a 3×3 color-transformation matrix to simulate how a color appears under different vision deficiencies. Use it in development to verify that color is never the only way you convey meaning.

Available types

TypeDescription
.protanopiaRed-blind — no red cone function
.deuteranopiaGreen-blind — no green cone function
.tritanopiaBlue-blind — no blue cone function
.achromatopsiaTotal color blindness — monochromatic
.protanomalyRed-weak — reduced red cone sensitivity
.deuteranomalyGreen-weak — reduced green cone sensitivity
.tritanomalyBlue-weak — reduced blue cone sensitivity
Simulate how a specific color looks:
let simulated = PrismColorBlindnessSimulator.simulate(
    theme.color(.brand),
    type: .deuteranopia
)
Apply a simulation overlay to a view in your Previews:
#Preview {
    AccountCard()
        .prismSimulateColorBlindness(.protanopia)
}
Color blindness simulation is a development tool only. Do not ship prismSimulateColorBlindness() in production builds — it is intended for design review and testing.

Focus order

VoiceOver reads views in the order they appear in the view hierarchy. When your layout uses overlapping views, z-stacks, or custom drawing, the traversal order may not match the logical reading order. Use prismFocusOrder() to set explicit sort priorities. Higher priority numbers are read first:
VStack {
    Text("Total due")
        .prismFocusOrder(priority: 20, label: "Total due")

    Text("$4,218.00")
        .prismFocusOrder(priority: 10, label: "4218 dollars")

    PrismButton("Pay now", variant: .filled) { await pay() }
        .prismFocusOrder(priority: 5, label: "Pay now button")
}
Validate a sequence programmatically in your tests:
let items = [
    PrismFocusOrderItem(id: "total-label", label: "Total due",     priority: 20),
    PrismFocusOrderItem(id: "total-value", label: "4218 dollars",  priority: 10),
    PrismFocusOrderItem(id: "pay-button",  label: "Pay now button", priority: 5),
]

let result = PrismFocusOrderValidator.validate(items)
XCTAssertTrue(result.isValid, result.warnings.joined(separator: "\n"))

Accessibility announcements

Use PrismAccessibilityAnnouncer to post screen-reader announcements programmatically — for example, when an async operation completes:
await viewModel.submitPayment()
PrismAccessibilityAnnouncer.announce("Payment submitted successfully", priority: .polite)
Post an announcement reactively when a binding changes with .prismAnnounce():
PrismProgressBar(value: uploadProgress)
    .prismAnnounce(
        when: uploadProgress,
        message: "Upload complete",
        priority: .assertive
    )
Use .polite to wait for current speech to finish, and .assertive to interrupt immediately for time-sensitive information.

Accessibility audit and testing

Runtime audit

.prismAccessibilityAudit() logs warnings for views missing accessibility labels during DEBUG builds. It has no effect in release builds.
VStack {
    // ...
}
.prismAccessibilityAudit("Payment form")
Check your console for lines beginning with to identify unlabeled elements.

Unit test assertions

PrismAccessibilityTest provides two static helpers you can call directly in XCTestCase:
// Verify minimum 44×44pt tap target
XCTAssertTrue(
    PrismAccessibilityTest.validateMinimumTapTarget(width: frame.width, height: frame.height)
)

// Verify WCAG AA contrast ratio
XCTAssertTrue(
    PrismAccessibilityTest.validateContrastRatio(
        foreground: theme.color(.onBackground),
        background: theme.color(.background)
    )
)

// Large text (18pt+) only needs 3:1
XCTAssertTrue(
    PrismAccessibilityTest.validateContrastRatio(
        foreground: theme.color(.onBackground),
        background: theme.color(.background),
        isLargeText: true
    )
)
These helpers use the same WCAG 2.x relative-luminance formula as PrismContrastChecker, so your unit tests and runtime checks stay consistent.