Skip to content
Components PulseList

PulseList

Settings screens, perfected.

Layout
interactive demo
Connections
Wi-FiHome Network — Connected
BluetoothAirPods Pro
About
Privacy & Security
Storage
92%

A live browser stand-in using the exact tokens, motion and API of the SwiftUI component.

Settings screens, perfected.

Rounded and plain lists with icon chips, chevrons and rich rows — the fastest way to a home-screen-quality Settings page.

Rounded / plain containers
Icon-chip rows with tints
Chevron and contextual rows
Section-aware styling
Usage
PulseList(.rounded) {
    PulseListRow("Wi-Fi", subtitle: "Connected", icon: "wifi", iconTint: .success)
    PulseListRow("Bluetooth", icon: "dot.radiowaves…", showsChevron: true)
}
Install
$ pulse add list

Source files

Sources/PulseUI/Components/PulseList/PulseList.swift
Full source Sources/PulseUI/Components/PulseList/PulseList.swift
import SwiftUI

// MARK: - PulseList

/// Styled list container with rows, separators, icons, and optional swipe actions.
public struct PulseList<Content: View>: View {
    @Environment(\.pulseTheme) private var theme

    let style: ListStyle
    let content: () -> Content

    public init(
        _ style: ListStyle = .rounded,
        @ViewBuilder content: @escaping () -> Content
    ) {
        self.style = style
        self.content = content
    }

    public var body: some View {
        Group {
            switch style {
            case .rounded:
                content()
                    .background(theme.colors.card)
                    .clipShape(RoundedRectangle(cornerRadius: theme.radius.xl, style: .continuous))
                    .overlay(
                        RoundedRectangle(cornerRadius: theme.radius.xl, style: .continuous)
                            .strokeBorder(theme.colors.border, lineWidth: 1)
                    )
            case .plain:
                content()
            case .inset:
                content()
                    .padding(.horizontal, theme.spacing.md)
                    .background(theme.colors.card)
                    .clipShape(RoundedRectangle(cornerRadius: theme.radius.lg, style: .continuous))
            }
        }
    }
}

// MARK: - PulseListRow

public struct PulseListRow: View {
    @Environment(\.pulseTheme) private var theme
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    let title: String
    let subtitle: String?
    let icon: String?
    let iconTint: Color?
    let showsChevron: Bool
    let action: (() -> Void)?

    @State private var isPressed = false

    public init(
        _ title: String,
        subtitle: String? = nil,
        icon: String? = nil,
        iconTint: Color? = nil,
        showsChevron: Bool = false,
        action: (() -> Void)? = nil
    ) {
        self.title = title
        self.subtitle = subtitle
        self.icon = icon
        self.iconTint = iconTint
        self.showsChevron = showsChevron
        self.action = action
    }

    public var body: some View {
        Button {
            action?()
            if action != nil {
                PulseHaptic.selection()
            }
        } label: {
            HStack(spacing: theme.spacing.md) {
                if let icon {
                    Image(systemName: icon)
                        .font(.subheadline.weight(.medium))
                        .foregroundStyle(iconTint ?? theme.colors.accent)
                        .frame(width: 34, height: 34)
                        .background(
                            RoundedRectangle(cornerRadius: 10, style: .continuous)
                                .fill((iconTint ?? theme.colors.accent).opacity(0.12))
                        )
                        .accessibilityHidden(true)
                }

                VStack(alignment: .leading, spacing: 2) {
                    Text(title)
                        .font(theme.typography.body.font)
                        .foregroundStyle(theme.colors.foreground)
                        .lineLimit(1)

                    if let subtitle {
                        Text(subtitle)
                            .font(theme.typography.footnote.font)
                            .foregroundStyle(theme.colors.foregroundSecondary)
                            .lineLimit(1)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)

                if showsChevron {
                    Image(systemName: "chevron.right")
                        .font(.caption.weight(.semibold))
                        .foregroundStyle(theme.colors.foregroundTertiary)
                        .accessibilityHidden(true)
                }
            }
            .padding(.horizontal, theme.spacing.lg)
            .padding(.vertical, 12)
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .scaleEffect(isPressed && action != nil ? 0.98 : 1)
        .onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in
            if action != nil {
                withAnimation(theme.motion.springSnappy) {
                    isPressed = pressing
                }
            }
        }, perform: {})
        .accessibilityElement(children: .combine)
        .accessibilityHint(subtitle ?? title)
        .accessibilityAddTraits(showsChevron ? .isButton : [])
    }
}

// MARK: - Types

public enum PulseListStyle: Sendable {
    case rounded
    case plain
    case inset
}

public typealias ListStyle = PulseListStyle