Autumn Sale50% offCOMPOT
Tiles & SelectablesPro ComponentiOS 16.0+Xcode 15+

Selectable Tile

Interactive card with active/selected border highlight for option grids.

SwiftUI Implementation

Pure Vanilla Swift • No Dependencies
selectableTile.swift
import SwiftUI

        struct UseSelectableTile: View {
            
            @State private var selection: SelectableOption = .option1
            
            private static func playHaptic() {
                
        #if os(iOS)
                let impact = UIImpactFeedbackGenerator(style: .soft)
                impact.impactOccurred()
        #endif
                
            }
            
            private let tileWidth: CGFloat = 160
            private let tileHeight: CGFloat = 140
            private let horizontalSpacing: CGFloat = 12
            private let carouselHorizontalPadding: CGFloat = 16
            
            var body: some View {
                VStack {
                    ScrollViewReader { scrollViewProxy in
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack(spacing: horizontalSpacing) {
                                ForEach(SelectableOption.allCases, id: \\.self) { option in
                                    SelectableTile(
                                        selectableOption: option,
                                        isSelected: option == selection
                                    )
                                    .frame(width: tileWidth, height: tileHeight)
                                    .contentShape(Rectangle())
                                    .onTapGesture {
                                        UseSelectableTile.playHaptic()
                                        withAnimation(.snappy(duration: 0.25, extraBounce: 0.05)) {
                                            selection = option
                                        }
                                    }
                                    .id(option)
                                    
                                }
                            }
                            .padding(.horizontal, carouselHorizontalPadding)
                            .padding(.vertical, 8)
                        }
                        .ignoresSafeArea(.container, edges: .horizontal)
                        .onChange(of: selection) {
                            withAnimation(.snappy(duration: 0.35, extraBounce: 0.08)) {
                                scrollViewProxy.scrollTo(selection, anchor: .leading)
                            }
                        }
                        .onAppear {
                            DispatchQueue.main.async {
                                withAnimation(.snappy(duration: 0.35)) {
                                    scrollViewProxy.scrollTo(selection, anchor: .center)
                                }
                            }
                        }
                        
                    }
                }
            }
        }

        enum SelectableOption: Identifiable, Hashable, CaseIterable, Codable {
            case option1
            case option2
            case option3
            
            var id: SelectableOption { self }
            
            var displayName: String {
                switch self {
                case .option1:
                    "Nice Option"
                case .option2:
                    "Better Option"
                case .option3:
                    "Great Option"
                }
            }
            
            var description: String {
                switch self {
                case .option1:
                    "This is a nice option to go for."
                case .option2:
                    "This is a better option to go for."
                case .option3:
                    "But this option is great!"
                }
            }
        }

        struct SelectableTile: View {
            
            let selectableOption: SelectableOption
            let isSelected: Bool
            
            private let cornerRadius: CGFloat = 12
            private let selectedStrokeWidth: CGFloat = 2.5
            private let checkmarkPadding: CGFloat = 6
            
            var body: some View {
                ZStack(alignment: .topTrailing) {
                    VStack(alignment: .leading, spacing: 5) {
                        Text(selectableOption.displayName)
                            .font(.system(size: 16))
                            .foregroundColor(.primary)
                            .lineLimit(2)
                            .minimumScaleFactor(0.8)
                        
                        Text(selectableOption.description)
                            .font(.system(size: 13))
                            .foregroundColor(.secondary)
                            .lineLimit(5)
                            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                        
                        Spacer(minLength: 0)
                    }
                    .padding()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(.thinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
                    .overlay {
                        if isSelected {
                            RoundedRectangle(cornerRadius: cornerRadius - (selectedStrokeWidth / 2))
                                .stroke(Color.accentColor, lineWidth: selectedStrokeWidth)
                                .padding(selectedStrokeWidth / 2)
                        }
                    }
                    
                    if isSelected {
                        Image(systemName: "checkmark.circle.fill")
                            .font(.title3)
                            .foregroundColor(Color.accentColor)
                            .background(Circle().fill(.background.opacity(0.6)).blur(radius: 1.0))
                            .padding(checkmarkPadding)
                            .transition(.scale.combined(with: .opacity))
                    }
                }
                .animation(.easeInOut(duration: 0.2), value: isSelected)
            }
        }

Ready to copy and paste directly into any Xcode project running iOS 16.0+.

Recommended Use Cases

  • Dashboard overviews, stats tracking, and metric highlights

Component Details

iOS Compatibility
iOS 16.0+
Xcode Version
Xcode 15+
Platform
iOS
License Tier
Pro
Themes & Contexts
dashboard
Keywords
selection carousel
C

Compot for iOS

Native SwiftUI playground app

Preview all 130+ components interactively on your device and copy code straight to Xcode with 1 tap.

Get Compot on the App Store