A token-driven SwiftUI design system with Dark Mode + Dynamic Type—built to feel native, predictable, and composable, while giving you a centralized way to evolve typography, color, spacing, and surface behavior over time.
Who is this for? Apps that want a strong SwiftUI foundation and theme evolution without rewriting view code.
💬 Join the discussion. Feedback and questions welcome
See it in action: Open Demo/GentleDesignSystemDemo.xcodeproj to explore the components. The demo app also supports editing and sharing JSON specs via the system Share Sheet.
.package(url: "https://github.com/gentle-giraffe-apps/GentleDesignSystem.git", from: "0.1.7")import GentleDesignSystem
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
GentleThemeRoot(theme: .default) {
ContentView()
}
}
}
}Text("Welcome")
.gentleText(.title_xl)
Text("Description")
.gentleText(.body_m, colorRole: .textSecondary)Button("Continue") { }
.gentleButton(.primary)
Button("Cancel") { }
.gentleButton(.secondary)VStack {
Text("Card content")
}
.gentleSurface(.card)This project enforces quality gates via CI and static analysis:
- CI: All commits to
mainmust pass GitHub Actions checks - Static analysis: DeepSource runs on every commit to
main.
The badge indicates the current number of outstanding static analysis issues. - Test coverage: Codecov reports line coverage for the
mainbranch
These checks are intended to keep the design system safe to evolve over time.
GentleDesignSystem is intentionally structured around three layers:
- Token Definitions (Codable, JSON-friendly)
- Runtime Resolution (Theme + Environment)
- SwiftUI Ergonomics (Modifiers & Extensions)
This separation keeps design intent clear, runtime behavior predictable, and future evolution safe.
flowchart TB
subgraph Tokens["Token Layer (Design-Time)"]
Spec[GentleDesignSystemSpec]
Spec --> Colors[GentleColorTokens]
Spec --> Typography[GentleTypographyTokens]
Spec --> Layout[GentleLayoutTokens]
Spec --> Visual[GentleVisualTokens]
Spec --> Buttons[GentleButtonTokens]
Spec --> Surfaces[GentleSurfaceTokens]
end
subgraph Runtime["Runtime Layer"]
Theme[GentleTheme]
Manager[GentleThemeManager]
Store[GentleFileThemeSpecStore]
Manager --> Theme
Store -.->|load/save| Manager
end
subgraph SwiftUI["SwiftUI Layer"]
Root[GentleThemeRoot]
Env[Environment Values .gentleTheme]
Modifiers[View Modifiers]
Root --> Env
Env --> Modifiers
end
Tokens --> Runtime
Runtime --> SwiftUI
flowchart TB
JSON[(JSON File)] -->|load| Store[GentleFileThemeSpecStore]
Store --> Manager[GentleThemeManager]
Manager --> Theme[GentleTheme]
Theme --> Resolve{Resolution}
Resolve -->|ColorScheme| ResolvedColor[Color]
Resolve -->|ContentSizeCategory| ResolvedFont[Font]
ResolvedColor --> View[SwiftUI View]
ResolvedFont --> View
View -->|.gentleText| Text
View -->|.gentleButton| Button
View -->|.gentleSurface| Surface
The design system is defined by a single JSON-friendly specification
(GentleDesignSystemSpec). The diagram below shows the structure of that
spec and how token groups are organized.
GentleDesignSystemSpec
│
├── colors: GentleColorTokens
│ │
│ └── pairByRole: [String: GentleColorPair]
│ │
│ ├── key = GentleColorRole.rawValue
│ └── value = GentleColorPair
│ ├── lightHex: String
│ └── darkHex: String
│
├── typography: GentleTypographyTokens
│ │
│ └── roles: [String: GentleTypographyRoleSpec]
│ │
│ ├── key = GentleTextRole.rawValue
│ └── value = GentleTypographyRoleSpec
│ ├── pointSize: Double
│ ├── weight: GentleFontWeightToken
│ ├── design: GentleFontDesignToken
│ ├── width: GentleFontWidthToken?
│ ├── relativeTo: GentleFontTextStyle
│ ├── lineSpacing: Double
│ ├── letterSpacing: Double
│ ├── isUppercased: Bool
│ └── colorRole: GentleColorRole
│
├── layout: GentleLayoutTokens
│ │
│ ├── scale: GentleSpacingScaleTokens
│ │ ├── xs / s / m / l / xl / xxl : Double
│ │ └── value(for: GentleSpacingToken) -> Double
│ │
│ ├── gap: GentleGapTokens
│ ├── grid: GentleGridSpacingTokens
│ ├── touch: GentleTouchTokens
│ │
│ └── inset: GentleInsetTokens
│ │
│ └── tokensByRole: [String: GentleAxisInsetTokens]
│ │
│ ├── key = GentleInsetRole.rawValue
│ └── value = GentleAxisInsetTokens
│ ├── horizontal: GentleSpacingToken
│ └── vertical: GentleSpacingToken
│
├── visual: GentleVisualTokens
│ │
│ ├── radii: GentleRadiusTokens
│ │ ├── small: Double
│ │ ├── medium: Double
│ │ ├── large: Double
│ │ └── pill: Double
│ │
│ └── shadows: GentleShadowTokens
│ ├── none: Double
│ ├── small: Double
│ └── medium: Double
│
├── buttons: GentleButtonTokens
│ │
│ ├── roles: [String: GentleButtonRoleSpec]
│ │ │
│ │ ├── key = GentleButtonRole.rawValue
│ │ └── value = GentleButtonRoleSpec
│ │ ├── shape: GentleButtonShape
│ │ ├── materialRole: GentleButtonMaterialRole
│ │ ├── borderRole: GentleButtonBorderRole
│ │ ├── animationRole: GentleButtonAnimationRole
│ │ ├── pressedScale: Double
│ │ ├── pressedOpacity: Double
│ │ └── usesNativeStyle: Bool
│ │
│ └── animations: [String: GentleButtonAnimationSpec]
│ │
│ ├── key = GentleButtonAnimationRole.rawValue
│ └── value = GentleButtonAnimationSpec
│ ├── pressedScale: Double
│ ├── pressedOpacity: Double
│ ├── duration: Double
│ ├── springResponse: Double
│ ├── springDamping: Double
│ └── springBlend: Double
│
└── surfaces: GentleSurfaceTokens
│
└── roles: [String: GentleSurfaceRoleSpec]
│
├── key = GentleSurfaceRole.rawValue
└── value = GentleSurfaceRoleSpec
├── backgroundStyle: GentleSurfaceBackgroundStyle
│ ├── .solid(colorRole: GentleColorRole)
│ ├── .material(material:, tintColorRole:, tintOpacity:)
│ └── .glass(fallbackMaterial:, fallbackColorRole:)
├── specularEffect: GentleSpecularEffect
├── specularStrength: Double
├── border: GentleColorPair
├── cornerRadius: Double
├── borderWidth: Double
├── shadowRadius: Double
├── shadowOpacity: Double
├── shadowOffsetX: Double
└── shadowOffsetY: Double
Why roles instead of direct values?
Roles provide stable identifiers that allow themes to evolve safely over time. Specs can change, presets can swap, and values can be overridden without breaking call sites or serialized themes.
The token layer defines what your design system means — not how it is rendered.
| Category | Types |
|---|---|
| Typography | GentleTextRole, GentleTypographyRoleSpec, GentleTypographyTokens |
| Colors | GentleColorRole, GentleColorPair, GentleColorTokens |
| Layout | GentleLayoutTokens, GentleSpacingToken, GentleGapTokens, GentleInsetTokens |
| Visual | GentleVisualTokens, GentleRadiusTokens, GentleShadowTokens |
| Buttons | GentleButtonRole, GentleButtonRoleSpec, GentleButtonTokens, GentleButtonAnimationRole |
| Surfaces | GentleSurfaceRole, GentleSurfaceRoleSpec, GentleSurfaceTokens |
All tokens are:
CodableSendable- JSON-friendly
This makes it easy to:
- Persist themes
- Load themes remotely
- Share tokens across platforms later
public struct GentleDesignSystemSpec: Codable, Sendable {
public var specVersion: String
public var colors: GentleColorTokens
public var typography: GentleTypographyTokens
public var layout: GentleLayoutTokens
public var visual: GentleVisualTokens
public var buttons: GentleButtonTokens
public var surfaces: GentleSurfaceTokens
}The default theme (.default) is simply one concrete spec.
At runtime, tokens are resolved into actual SwiftUI values.
GentleTheme:
- Owns a
GentleDesignSystemSpec - Resolves:
- Colors per
ColorScheme - Fonts per
ContentSizeCategory(Dynamic Type)
- Colors per
@Environment(\.gentleTheme) var themeTypography resolution uses UIFontMetrics to correctly scale custom font sizes while remaining anchored to Apple's semantic text styles.
This ensures:
- Accessibility scaling works correctly
- Custom point sizes remain proportional
- Future Dynamic Type changes remain safe
For convenient access to the theme in views:
// Access resolved theme values
@GentleDesignRuntime private var design
// Use in view
design.color(.textPrimary) // Color for current scheme
design.layout.stack.regular // CGFloat spacing value
design.buttons // Button tokensSwiftUI environments flow top-down.
By wrapping your app root with:
GentleThemeRoot {
ContentView()
}you ensure that:
- All child views receive the same theme
- Previews behave consistently
- Theme overrides are easy later (per scene, per feature, per preview)
GentleThemeRoot is intentionally lightweight — it only injects a single environment value.
This avoids:
- Global singletons
- Static state
- Implicit magic
GentleDesignSystem exposes ergonomic APIs while keeping logic centralized.
Text("Hello")
.gentleText(.headline_m)Internally:
- Resolves typography via
GentleTheme - Applies font, width, design, spacing, color
- Honors Dynamic Type automatically
VStack { ... }
.gentleSurface(.card)Surfaces apply:
- Background color
- Padding (when appropriate)
- Corner radius
- Borders or shadows
The role-based API avoids "magic numbers" leaking into views.
Button("Save") { }
.gentleButton(.primary)Buttons are:
- Styled via
ButtonStyle - Fully theme-driven
- Support configurable animations
- Easily extendable for new roles
For apps that need runtime theme editing or persistence:
@main
struct MyApp: App {
@State private var manager = GentleThemeManager(theme: .default)
var body: some Scene {
WindowGroup {
GentleThemeRoot(theme: manager.theme) {
ContentView()
}
.environment(\.gentleThemeManager, manager)
}
}
}@GentleThemeManagerRuntime private var manager
// Save current theme to disk
try manager.save()
// Load persisted theme
try manager.load()
// Get bindings for editing
manager.typographyBinding(for: .body_m)
manager.colorBinding(for: .primaryCTA)GentleFileThemeSpecStore handles JSON persistence to Application Support:
let store = GentleFileThemeSpecStore(fileName: "my-theme.json")
let manager = GentleThemeManager(theme: .default, store: store)GentleDesignSystem includes 9 built-in theme presets, each designed for different use cases and aesthetics.
// Get all available presets
let presets = GentleDesignSystemSpec.allPresets
// Each preset provides:
// - name: Display name (e.g., "Gentle Default")
// - summary: Brief tagline
// - description: Detailed explanation
// - purpose: When to use this preset
// - systemImageString: SF Symbol name for UI
// - spec: The actual GentleDesignSystemSpec| Preset | Summary | Best For |
|---|---|---|
| Gentle Default | Calm, balanced foundation | Versatile starting point with clean hierarchy |
| Classic Tan | Warm, timeless with earthy tones | Apps benefiting from warmth and heritage |
| Modern Gray | Sleek, minimal with neutral foundations | Business apps where clarity is paramount |
| Soft Green | Fresh, natural with calming accents | Wellness, productivity, calm focus |
| Editorial Paper | Refined, print-inspired reading | Content-heavy apps, long-form reading |
| Technical Blue | Precise, trustworthy with blue highlights | Developer tools, dashboards |
| Bold Orange | Vibrant, energetic with strong presence | Apps that motivate action |
| Elegant Purple | Sophisticated, luxurious with rich tones | Lifestyle, creative, premium apps |
| Compact Mint | Dense, efficient with fresh accents | Data-rich interfaces |
// Apply a preset to your theme manager
@GentleThemeManagerRuntime private var manager
// Find and apply a preset
if let editorialPreset = GentleDesignSystemSpec.allPresets.first(where: { $0.name == "Editorial Paper" }) {
manager.theme.editableSpec = editorialPreset.spec
}The demo app includes a ThemePickerView that displays all presets as interactive cards. Each card previews the preset's typography and colors using the preset's own theme:
ForEach(presets, id: \.name) { preset in
let previewTheme = GentleTheme(
defaultSpec: preset.spec,
editableSpec: preset.spec
)
Button {
themeManager.theme.editableSpec = preset.spec
} label: {
GentleThemeRoot(theme: previewTheme) {
// Card content renders with the preset's own styling
ThemePresetCard(preset: preset)
}
}
}17 semantic text roles organized by size ramp (xxl > xl > l > ml > m > ms > s):
| Ramp | Roles |
|---|---|
| XXL | largeTitle_xxl |
| XL | title_xl |
| L | title2_l |
| ML | title3_ml |
| M | headline_m, body_m, bodySecondary_m, monoCode_m, primaryButtonTitle_m, secondaryButtonTitle_m, tertiaryButtonTitle_m, quaternaryButtonTitle_m |
| MS | callout_ms, subheadline_ms |
| S | footnote_s, caption_s, caption2_s |
Each role resolves to a GentleTypographyRoleSpec containing: pointSize, weight, design, width, relativeTo, lineSpacing, letterSpacing, isUppercased, and colorRole.
primary · secondary · tertiary · quaternary · destructive
| Animation | Description |
|---|---|
unknown |
No animation |
subtlePress |
Subtle press feedback |
squish |
Squish effect on press |
pop |
Pop effect |
bouncy |
Bouncy spring animation |
springBack |
Shrinks on press, springs back past original size before settling |
appBackground · card · cardElevated · surfaceOverlay
| Category | Roles |
|---|---|
| Text (7) | textPrimary, textSecondary, textTertiary, textOnPrimaryCTA, textOnDestructive, textOnOverlay, textOnOverlaySecondary |
| Surfaces (6) | background, surfaceBase, surfaceOverlay, surfaceTint, surfaceSpecular, borderSubtle |
| Actions (2) | primaryCTA, destructive |
| Theme (2) | themePrimary, themeSecondary |
Use semantic groupings: GentleColorRole.textRoles, .surfaceRoles, .actionRoles, .themeRoles
Use membership checks: role.isTextRole, .isSurfaceRole, .isActionRole, .isThemeRole
xs (4) · s (8) · m (12) · l (16) · xl (24) · xxl (32)
small (8) · medium (12) · large (20) · pill (999)
- iOS 18.0+
- Swift 6.1+
Portions of drafting and editorial refinement in this repository were accelerated using large language models (including ChatGPT, Claude, and Gemini) under direct human design, validation, and final approval. All technical decisions, code, and architectural conclusions are authored and verified by the repository maintainer.
