Skeleton

iOS 16.4+iPadOS 16.4+

Shows a placeholder while content is loading.

FLXSkeleton avatar and two text lines
import SwiftUIstruct SkeletonUsageExample: View {    var body: some View {        HStack(spacing: FLXSpacing.lg) {            FLXSkeleton()                .frame(width: 48, height: 48)                .clipShape(Circle())            VStack(alignment: .leading, spacing: FLXSpacing.sm) {                FLXSkeleton()                    .frame(width: 220, height: 16)                FLXSkeleton()                    .frame(width: 160, height: 16)            }        }        .padding(FLXSpacing.xl)    }}

Installation

Requires the design system foundation (tokens and theme).

pnpm dlx shadcn@latest add @swiftui-flx/ios-skeleton

Usage

FLXSkeleton fills the space it's given, so set its size with .frame and its shape with .clipShape.

FLXSkeleton()    .frame(width: 220, height: 16)

Card

Compose skeletons in the same layout as the content they stand in for, so nothing moves when it loads.

FLXCard with skeleton placeholders
FLXCard {  FLXCardHeader {      FLXSkeleton()          .frame(width: 180, height: 16)      FLXSkeleton()          .frame(width: 120, height: 12)  }  FLXCardContent {      FLXSkeleton()          .frame(height: 120)  }}

Redacted

To turn a view you already have into a placeholder, use SwiftUI's .redacted(reason: .placeholder). It replaces text and images with blocks of the same size, so you don't build a second layout. Use .unredacted() on a part that should stay visible.

A profile row redacted as a placeholder
HStack(spacing: FLXSpacing.md) {  Image(systemName: "person.crop.circle.fill")      .flxFont(.largeTitle)  VStack(alignment: .leading) {      Text("Olivia Martin")          .flxFont(.headline)      Text("olivia@example.com")          .flxFont(.subheadline)  }}.redacted(reason: .placeholder)

Accessibility

FLXSkeleton is hidden from VoiceOver, and its pulse stops when Reduce Motion is on. Tell VoiceOver that the area is loading on the container:

VStack {    FLXSkeleton()        .frame(height: 120)}.accessibilityElement().accessibilityLabel("Loading")

Custom styling

FLXSkeletonStyle is an FLX protocol. Override it for a subtree via .flxSkeletonStyle(_:):

FLXSkeleton custom styling
struct FLXStaticSkeletonStyle: FLXSkeletonStyle {  func makeBody(configuration: Configuration) -> some View {      Capsule()          .fill(.quaternary)  }}VStack(alignment: .leading, spacing: FLXSpacing.sm) {  FLXSkeleton()      .frame(width: 220, height: 16)  FLXSkeleton()      .frame(width: 160, height: 16)}.flxSkeletonStyle(FLXStaticSkeletonStyle())

API Reference

FLXSkeleton

init()

Filled with theme.colors.accent and theme.radius.md, pulsing between full and half opacity.