-
Notifications
You must be signed in to change notification settings - Fork 0
Buttons
DFButton is DesignFoundation's interactive button component. It wraps a label string, an optional semantic role, and an action closure into a fully-themed, press-animated, accessibility-compliant button. Five built-in styles cover the full range of visual emphasis; the style system is open for extension.
Import
import DesignFoundationMinimum platform: iOS 18+, macOS 15+, visionOS 2+
DFButton("Save Changes") { save() }That one line renders a filled, themed button with a press animation, the correct label typography, disabled-state handling, and VoiceOver support.
public init(
_ label: String,
role: DFButtonRole? = nil,
action: @escaping () -> Void
)| Parameter | Type | Default | Description |
|---|---|---|---|
label |
String |
required | Visible button text. Also used verbatim as the VoiceOver accessibility label. |
role |
DFButtonRole? |
nil |
Semantic role. .destructive substitutes theme.colors.destructive for the accent color across all styles and adds a "Destructive action" VoiceOver hint. .cancel carries semantic meaning only — no visual change. |
action |
@escaping () -> Void |
required | Closure executed on tap. Suppressed automatically when the button is disabled. |
// Apply to a single button
DFButton("Cancel") { dismiss() }
.dfButtonStyle(.outlined)
// Apply to a container — all child DFButtons inherit
VStack {
DFButton("Option A") { selectA() }
DFButton("Option B") { selectB() }
}
.dfButtonStyle(.tinted)The default style when no modifier is applied is .filled.
The highest-emphasis style. A solid colored background with white text. Use for the single primary action on a screen.
- Background:
theme.colors.interactiveFill(normal) → opacity 0.75 (pressed) →theme.colors.interactiveDisabled(disabled) - Foreground:
Color.white(normal) →theme.colors.textDisabled(disabled) - Press effect: scale 0.97 + background opacity 0.75, animated at
theme.animation.fast
DFButton("Create Account") { createAccount() }
DFButton("Delete Project", role: .destructive) { deleteProject() }
.dfButtonStyle(.filled)A 1.5 pt stroke border with no fill. Use for secondary actions alongside a filled primary button.
- Border: 1.5 pt
theme.colors.primary(normal) →theme.colors.border(disabled) - Foreground:
theme.colors.primary(normal) →theme.colors.textDisabled(disabled)
DFButton("Cancel") { dismiss() }
.dfButtonStyle(.outlined)
DFButton("Remove Member", role: .destructive) { removeMember() }
.dfButtonStyle(.outlined)Text and padding only — no background, no border. The lowest emphasis style, for tertiary actions or inline textual links.
- Foreground:
theme.colors.primary(normal) →theme.colors.textDisabled(disabled) - Press effect: overall opacity drops to 0.6
DFButton("Learn more") { openURL(learnMoreURL) }
.dfButtonStyle(.ghost)
DFButton("Skip for now") { skip() }
.dfButtonStyle(.ghost)A translucent, color-tinted background. Sits between filled and outlined in visual weight.
- Background fill:
theme.colors.primary.opacity(0.15)(normal) →0.25(pressed) - Foreground:
theme.colors.primary(normal)
DFButton("Add to Favorites") { favorite() }
.dfButtonStyle(.tinted)A vibrancy-backed material button for use over images, blurred panels, or layered surfaces.
- Background:
.regularMaterial - Foreground:
theme.colors.textPrimary - Press effect: scale 0.97
if #available(iOS 26, macOS 26, *) {
DFButton("Open") { openPhoto() }
.dfButtonStyle(.glass)
}| Role | Visual effect | VoiceOver hint |
|---|---|---|
.destructive |
Substitutes theme.colors.destructive for the primary color in every built-in style |
"Destructive action" |
.cancel |
No visual change | No hint |
nil (default) |
Primary theme color everywhere | No hint |
DFButton("Delete Account", role: .destructive) { deleteAccount() }
DFButton("Remove", role: .destructive) { remove() }
.dfButtonStyle(.outlined)
DFButton("Cancel", role: .cancel) { dismiss() }
.dfButtonStyle(.outlined)DFButton("Continue") { advance() }
.disabled(!formIsValid)Disabled appearances by style:
| Style | Background | Foreground |
|---|---|---|
| Filled | theme.colors.interactiveDisabled |
theme.colors.textDisabled |
| Outlined | None; border at 50% opacity |
theme.colors.textDisabled at 50% |
| Ghost | None |
theme.colors.textDisabled at 50% |
| Tinted | theme.colors.interactiveDisabled |
theme.colors.textDisabled |
| Glass |
.regularMaterial at 50% |
theme.colors.textDisabled |
DFButton("Sign In") { signIn() }
.frame(maxWidth: .infinity)| Feature | What is applied |
|---|---|
| VoiceOver label |
.accessibilityLabel(label) — the label string |
| Trait | .accessibilityAddTraits(.isButton) |
| Destructive hint |
.accessibilityHint("Destructive action") when role == .destructive
|
| Reduced Motion | Scale animations suppressed when Reduce Motion is enabled |
HStack(spacing: theme.spacing.md) {
DFButton("Cancel") { onCancel() }
.dfButtonStyle(.outlined)
DFButton("Confirm") { onConfirm() }
.dfButtonStyle(.filled)
}VStack(spacing: theme.spacing.md) {
DFButton("Create Free Account") { createAccount() }
.frame(maxWidth: .infinity)
DFButton("I already have an account") { signIn() }
.frame(maxWidth: .infinity)
.dfButtonStyle(.outlined)
DFButton("Continue as Guest") { continueAsGuest() }
.frame(maxWidth: .infinity)
.dfButtonStyle(.ghost)
}
.padding(.horizontal, theme.spacing.xl)DFButton("Delete My Account", role: .destructive) {
showDeleteConfirmation = true
}
.dfButtonStyle(.outlined)
.dfAlert(
isPresented: $showDeleteConfirmation,
alert: DFAlert(
title: "Delete your account?",
message: "All projects, settings, and data will be permanently removed.",
actions: [
DFAlertAction(title: "Cancel", role: .cancel) { },
DFAlertAction(title: "Delete Account", role: .destructive) {
deleteAccount()
},
]
)
)public protocol DFButtonStyle {
associatedtype Body: View
@ViewBuilder func makeBody(configuration: DFButtonStyleConfiguration) -> Body
}DFButtonStyleConfiguration exposes:
| Property | Type | Description |
|---|---|---|
label |
AnyView |
The rendered label |
isPressed |
Bool |
true during an active press |
isDisabled |
Bool |
Mirrors @Environment(\.isEnabled)
|
role |
DFButtonRole? |
.destructive, .cancel, or nil
|
theme |
DFTheme |
The active theme |
struct CapsuleButtonStyle: DFButtonStyle, Sendable {
func makeBody(configuration: DFButtonStyleConfiguration) -> some View {
let theme = configuration.theme
let color = configuration.role == .destructive
? theme.colors.destructive
: theme.colors.primary
configuration.label
.font((theme.components.button.labelStyle ?? theme.typography.label).font)
.foregroundStyle(configuration.isDisabled ? theme.colors.textDisabled : Color.white)
.padding(.horizontal, theme.spacing.xl)
.padding(.vertical, theme.spacing.sm)
.background(
Capsule().fill(configuration.isDisabled
? theme.colors.interactiveDisabled
: color.opacity(configuration.isPressed ? 0.8 : 1.0))
)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.spring(response: 0.25, dampingFraction: 0.6), value: configuration.isPressed)
}
}
extension DFButtonStyle where Self == CapsuleButtonStyle {
static var capsule: CapsuleButtonStyle { CapsuleButtonStyle() }
}
DFButton("Subscribe") { subscribe() }
.dfButtonStyle(.capsule)- Getting-Started — Package installation, scene root setup
- Theming — Complete token reference and theme presets
-
Overlays-and-Feedback —
DFAlert,DFToastQueue,DFModal,DFSheet -
Text-Fields-and-Inputs —
DFTextField,DFSecureField,DFValidatedTextField