Components PulseAlert
PulseAlert
Say it clearly, say it kindly.
interactive demo
SavedYour changes are synced to all devices.
Heads upA new update is available. It takes 2 minutes to install.
Storage almost fullYou are using 92% of your plan.
Connection lostCheck your network and try again.
A live browser stand-in using the exact tokens, motion and API of the SwiftUI component.
Want all components at once?
Open the studio
Say it clearly, say it kindly.
Inline alerts with semantic icons, tone-colored accents and dismiss control. Info, success, warning and error in one consistent shape.
4 semantic variants
Automatic tone icons
Optional dismiss (with haptics)
Token-driven background wash
Usage
PulseAlert(.info, title: "Heads up", message: "New update available.")
PulseAlert(.error, title: "Connection lost", message: "Check your network.") Install
$ pulse add alert Source files
Sources/PulseUI/Components/PulseAlert/PulseAlert.swift
Full source Sources/PulseUI/Components/PulseAlert/PulseAlert.swift
import SwiftUI
// MARK: - PulseAlert
/// Premium inline notification banner. Animated, icon-driven, dismissible.
public struct PulseAlert: View {
@Environment(\.pulseTheme) private var theme
@Environment(\.accessibilityReduceMotion) private var reduceMotion
let tone: PulseTone
let title: String
let message: String?
let icon: String?
let showsDismiss: Bool
let onDismiss: (() -> Void)?
@State private var isVisible = false
public init(
_ tone: PulseTone = .info,
title: String,
message: String? = nil,
icon: String? = nil,
showsDismiss: Bool = true,
onDismiss: (() -> Void)? = nil
) {
self.tone = tone
self.title = title
self.message = message
self.icon = icon
self.showsDismiss = showsDismiss
self.onDismiss = onDismiss
}
public var body: some View {
HStack(alignment: .top, spacing: theme.spacing.md) {
iconView
.frame(width: 20)
.padding(.top, 2)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(theme.typography.callout.font)
.fontWeight(.semibold)
.foregroundStyle(theme.colors.foreground)
.fixedSize(horizontal: false, vertical: true)
if let message {
Text(message)
.font(theme.typography.subheadline.font)
.foregroundStyle(theme.colors.foregroundSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if showsDismiss {
Button {
withAnimation(theme.motion.springSnappy) {
onDismiss?()
}
PulseHaptic.impact(.light)
} label: {
Image(systemName: "xmark")
.font(.caption.weight(.medium))
.foregroundStyle(theme.colors.foregroundTertiary)
.frame(width: 24, height: 24)
.contentShape(Rectangle())
}
.buttonStyle(PulseIconButtonStyle())
.padding(.top, -2)
.accessibilityLabel("Dismiss alert")
}
}
.padding(theme.spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundView)
.overlay(
RoundedRectangle(cornerRadius: theme.radius.lg, style: .continuous)
.strokeBorder(backgroundFill, lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: theme.radius.lg, style: .continuous))
.shadow(color: toneColor.opacity(0.08), radius: 8, y: 4)
.offset(y: isVisible ? 0 : 12)
.scaleEffect(isVisible ? 1 : 0.97)
.opacity(isVisible ? 1 : 0)
.onAppear {
guard !reduceMotion else {
isVisible = true
return
}
withAnimation(theme.motion.spring) {
isVisible = true
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel(title)
.accessibilityHint(message ?? "")
}
@ViewBuilder
private var iconView: some View {
ZStack {
Circle()
.fill(toneColor.opacity(0.15))
.frame(width: 30, height: 30)
Image(systemName: icon ?? defaultIcon)
.font(.caption.weight(.semibold))
.foregroundStyle(toneColor)
}
}
private var backgroundView: some View {
backgroundFill.opacity(0.6)
}
private var backgroundFill: Color {
toneColor.opacity(0.12)
}
private var defaultIcon: String {
switch tone {
case .success: return "checkmark.circle.fill"
case .warning: return "exclamationmark.triangle.fill"
case .error: return "xmark.circle.fill"
case .info: return "info.circle.fill"
case .accent: return "sparkles"
case .primary: return "circle.fill"
case .neutral: return "circle"
}
}
private var toneColor: Color {
switch tone {
case .neutral: return theme.colors.secondary
case .primary: return theme.colors.primary
case .accent: return theme.colors.accent
case .success: return theme.colors.success
case .warning: return theme.colors.warning
case .error: return theme.colors.error
case .info: return theme.colors.info
}
}
}