MIT Licensed · Swift Package Manager

A Kanban board that
acts like a native control.

Conform two of your own types, hand it a binding, and you have a drag-and-drop board with a full keyboard move-mode and VoiceOver parity — not just pointer drag with accessibility bolted on after.

0
Dependencies
31
Passing Tests
3
Input Methods
iOS 17+
macOS 14+
MIT
License

Every existing SwiftUI Kanban is pointer-only.

That's the gap this fills. Keyboard and VoiceOver users aren't an afterthought bolted onto a drag gesture — they call the exact same move-resolution function pointer drag does, so behavior can't quietly diverge between input methods.

No wrapper types
Conform your existing card and column structs to KanbanCard / KanbanColumn. No board-model type to keep in sync with your real data — the board drives reordering directly on your Binding<[Column]>.
Content is entirely yours
AdvancedKanban renders card and column chrome — background, border, corner radius, drag/WIP-warning state. What's inside a card is your own @ViewBuilder, so it never fights a fixed schema.
Real WIP limits
Set wipLimit per column, choose .warnOnly or .preventDrop, and it's enforced identically whether the move came from a mouse, a keyboard, or VoiceOver.
Native on macOS, not ported
Hover feedback, a grab cursor, and a drag-activation threshold tuned for a pointer instead of a finger. Built to be embedded in a real Mac app, not just to compile for one.

Add a board in three steps.

This is the whole integration — no configuration file, no board-state class to instantiate first.

  1. Conform your card type to KanbanCard

    It only needs Identifiable and Equatable — most model types already qualify.

    import AdvancedKanban
    
    struct Task: KanbanCard {
        let id: UUID
        var title: String
    }
  2. Conform your column type to KanbanColumn

    cards order is the board's display order — no separate sort index. wipLimit is optional; isCollapsed is a plain stored property the board toggles on tap.

    struct Stage: KanbanColumn {
        let id: UUID
        var name: String
        var cards: [Task] = []
        var wipLimit: Int? = nil
        var isCollapsed: Bool = false
    }
  3. Call KanbanBoard

    Give it a binding and two view builders. That's a complete, draggable, keyboard- and VoiceOver-accessible board.

    import SwiftUI
    import AdvancedKanban
    
    struct ContentView: View {
        @State private var columns: [Stage] = [
            Stage(id: UUID(), name: "To Do", cards: [Task(id: UUID(), title: "Write README")]),
            Stage(id: UUID(), name: "In Progress", wipLimit: 3),
            Stage(id: UUID(), name: "Done"),
        ]
    
        var body: some View {
            KanbanBoard(
                columns: $columns,
                wipLimitBehavior: .preventDrop,
                onMove: { move in
                    // persist however you like — SwiftData, a network call, analytics
                },
                columnTitle: { $0.name },   // real names in VoiceOver's "Move to "
                cardContent: { task in
                    Text(task.title).padding(8)
                },
                columnHeader: { stage in
                    Text(stage.name).font(.headline)
                }
            )
        }
    }
How onMove works

onMove fires after the board has already mutated your columns binding — it's purely a persistence hook. You never diff arrays yourself; the board did the mutation, the closure just tells you what happened.


KanbanBoard initializer

Every parameter, in order.

ParameterTypeWhat it does
columnsBinding<[Column]>Your data. The board mutates it directly on drop.
wipLimitBehaviorWIPLimitBehavior.warnOnly (default) or .preventDrop.
onMove((KanbanMove) -> Void)?Fires after mutation — your persistence hook.
columnTitle((Column) -> String)?Optional. Gives VoiceOver a real column name instead of the raw id.
cardContent@ViewBuilder (Card) -> some ViewRequired. What's inside a card.
columnHeader@ViewBuilder (Column) -> some ViewRequired. What's inside a column's header.

See Theming for KanbanTheme, Accessibility for the full keyboard and VoiceOver model, and Persistence for the optional SwiftData adapter.

Read the design spec.

Every architectural decision — why the drag engine isn't built on Transferable, how the keyboard/VoiceOver/pointer paths share one code path, the theming boundary — is written down.

Read the design spec →

A board is one screen. What about the other forty?

AdvancedKanban is a single component. If you're assembling the rest of the app around it, this is what we build the rest of our own apps out of.

DesignFoundation Pro
47 production-ready SwiftUI screens across 9 verticals — AI Chat, CRM, Analytics, E-commerce, Social, Project Management, Onboarding, Settings, Documents — plus 29 SwiftUI UI blocks and 18 shell layouts. One purchase, unlimited projects.

Browse the SwiftUI screen templates →
DesignFoundation
The free, MIT-licensed theming engine underneath Pro — typed design tokens for color, typography, spacing, and radius, with zero hardcoded values. The same primitive both projects theme from.

View the free SwiftUI theming library →