Skip to main content

Design System & UI Components

Design System Overview

A design system provides a single source of truth for typography, spacing, colors, and reusable components.

Design Tokens

Define design tokens as constants to ensure consistency.

iOS - Design Tokens

// App/Core/DesignSystem/DSTokens.swift
enum DS {
enum Spacing {
static let xs: CGFloat = 4
static let s: CGFloat = 8
static let m: CGFloat = 16
static let l: CGFloat = 24
static let xl: CGFloat = 32
static let xxl: CGFloat = 48
}

enum Radius {
static let s: CGFloat = 4
static let m: CGFloat = 8
static let l: CGFloat = 12
static let xl: CGFloat = 16
}

enum Typography {
static let largeTitle = Font.system(size: 34, weight: .bold)
static let title1 = Font.system(size: 28, weight: .bold)
static let title2 = Font.system(size: 22, weight: .bold)
static let title3 = Font.system(size: 20, weight: .semibold)
static let headline = Font.system(size: 17, weight: .semibold)
static let body = Font.system(size: 17, weight: .regular)
static let callout = Font.system(size: 16, weight: .regular)
static let subheadline = Font.system(size: 15, weight: .regular)
static let footnote = Font.system(size: 13, weight: .regular)
static let caption = Font.system(size: 12, weight: .regular)
}
}

Android - Design Tokens

// app/core/designsystem/DSTokens.kt
object DS {
object Spacing {
val xs = 4.dp
val s = 8.dp
val m = 16.dp
val l = 24.dp
val xl = 32.dp
val xxl = 48.dp
}

object Radius {
val s = 4.dp
val m = 8.dp
val l = 12.dp
val xl = 16.dp
}
}

// Use Material Theme for typography and colors
// themes/Theme.kt
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme()

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

Flutter - Design Tokens

// lib/core/design_system/ds_tokens.dart
class DS {
static const spacing = _Spacing();
static const radius = _Radius();
static const typography = _Typography();
}

class _Spacing {
const _Spacing();

final double xs = 4.0;
final double s = 8.0;
final double m = 16.0;
final double l = 24.0;
final double xl = 32.0;
final double xxl = 48.0;
}

class _Radius {
const _Radius();

final double s = 4.0;
final double m = 8.0;
final double l = 12.0;
final double xl = 16.0;
}

class _Typography {
const _Typography();

final TextStyle largeTitle = const TextStyle(
fontSize: 34,
fontWeight: FontWeight.bold,
);

final TextStyle title1 = const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
);

final TextStyle body = const TextStyle(
fontSize: 17,
fontWeight: FontWeight.normal,
);
}

Reusable Components

Create reusable components with consistent styling.

iOS - Primary Button

// App/Core/DesignSystem/Components/PrimaryButton.swift
struct PrimaryButton: View {
let title: String
let action: () -> Void
var isLoading: Bool = false
var isEnabled: Bool = true

var body: some View {
Button(action: action) {
ZStack {
Text(title)
.opacity(isLoading ? 0 : 1)

if isLoading {
ProgressView()
.tint(.white)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, DS.Spacing.m)
.font(DS.Typography.headline)
}
.background(isEnabled ? Color.accentColor : Color.gray)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: DS.Radius.l))
.disabled(!isEnabled || isLoading)
}
}

// Usage
PrimaryButton(
title: "Login",
action: { viewModel.login() },
isLoading: viewModel.isLoading
)

Android - Primary Button

// app/core/designsystem/components/PrimaryButton.kt
@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isLoading: Boolean = false,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(56.dp),
enabled = enabled && !isLoading,
shape = RoundedCornerShape(DS.Radius.l)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text(
text = text,
style = MaterialTheme.typography.labelLarge
)
}
}
}

// Usage
PrimaryButton(
text = "Login",
onClick = { viewModel.login() },
isLoading = state.isLoading
)

Flutter - Primary Button

// lib/core/design_system/components/primary_button.dart
class PrimaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;

const PrimaryButton({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
});


Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DS.radius.l),
),
),
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(text),
),
);
}
}

// Usage
PrimaryButton(
text: 'Login',
onPressed: () => context.read<LoginBloc>().add(const LoginSubmitted()),
isLoading: state is LoginLoading,
)

Accessibility

Minimum Requirements

  1. Touch Targets: Minimum 44x44 points (iOS) / 48dp (Android)
  2. Contrast Ratios: WCAG AA minimum (4.5:1 for normal text, 3:1 for large text)
  3. Accessible Labels: All interactive elements must have labels
  4. Dynamic Type/Font Scaling: Support system font size preferences
  5. Screen Reader Support: Semantic markup and proper navigation

iOS - Accessibility

// Accessible button
Button("Delete") {
deleteItem()
}
.accessibilityLabel("Delete item")
.accessibilityHint("Removes this item from your list")

// Accessible image
Image(systemName: "star.fill")
.accessibilityLabel("Favorite")
.accessibilityAddTraits(.isButton)

// Dynamic Type support (automatic with SwiftUI Text)
Text("Hello")
.font(DS.Typography.body) // Automatically scales

// Custom scalable font
Text("Custom")
.font(.custom("MyFont", size: 17, relativeTo: .body))

Android - Accessibility

// Accessible button
IconButton(
onClick = { deleteItem() },
modifier = Modifier.semantics {
contentDescription = "Delete item"
role = Role.Button
}
) {
Icon(Icons.Default.Delete, contentDescription = null)
}

// Minimum touch target
Button(
onClick = { },
modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
) {
Icon(Icons.Default.Star, contentDescription = "Favorite")
}

// Font scaling support (automatic with MaterialTheme)
Text(
text = "Hello",
style = MaterialTheme.typography.bodyLarge // Scales with system settings
)

Flutter - Accessibility

// Accessible button
IconButton(
icon: const Icon(Icons.delete),
onPressed: deleteItem,
tooltip: 'Delete item',
)

// Semantic label
Semantics(
label: 'Profile picture',
button: true,
child: GestureDetector(
onTap: openProfile,
child: CircleAvatar(backgroundImage: NetworkImage(user.avatarUrl)),
),
)

// Minimum touch target
SizedBox(
width: 48,
height: 48,
child: IconButton(
icon: const Icon(Icons.star),
onPressed: toggleFavorite,
),
)

// Font scaling support (use TextStyle, not hardcoded sizes)
Text(
'Hello',
style: Theme.of(context).textTheme.bodyLarge, // Scales with system
)

Dark Mode

Support both light and dark themes using semantic colors.

iOS - Dark Mode

// Use system colors or asset catalog with light/dark variants
struct ProfileView: View {
var body: some View {
VStack {
Text("Profile")
.foregroundColor(.primary) // Adapts to theme

Rectangle()
.fill(Color(.systemBackground)) // Adapts to theme
}
.background(Color(.secondarySystemBackground))
}
}

// Custom colors with dark mode support
extension Color {
static let customBackground = Color("CustomBackground") // Asset catalog
}

Android - Dark Mode

// Define color schemes
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
background = Color.White,
onBackground = Color.Black
)

private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFBB86FC),
onPrimary = Color.Black,
background = Color(0xFF121212),
onBackground = Color.White
)

@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
content = content
)
}

// Usage - colors automatically adapt
Text(
text = "Hello",
color = MaterialTheme.colorScheme.onBackground
)

Flutter - Dark Mode

// Define themes
final lightTheme = ThemeData(
brightness: Brightness.light,
colorScheme: const ColorScheme.light(
primary: Color(0xFF6200EE),
onPrimary: Colors.white,
background: Colors.white,
onBackground: Colors.black,
),
);

final darkTheme = ThemeData(
brightness: Brightness.dark,
colorScheme: const ColorScheme.dark(
primary: Color(0xFFBB86FC),
onPrimary: Colors.black,
background: Color(0xFF121212),
onBackground: Colors.white,
),
);

// In MaterialApp
MaterialApp(
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ThemeMode.system, // Follows system setting
home: HomePage(),
)

// Usage - colors automatically adapt
Text(
'Hello',
style: TextStyle(color: Theme.of(context).colorScheme.onBackground),
)

Localization

iOS - Localization

// Localizable.strings (English)
"welcome_title" = "Welcome, %@!";
"login_button" = "Login";

// Usage
Text(String(format: NSLocalizedString("welcome_title", comment: ""), userName))

// With SwiftUI
Text("login_button") // Automatically localized if key exists

Android - Localization

<!-- res/values/strings.xml (English) -->
<resources>
<string name="welcome_title">Welcome, %1$s!</string>
<string name="login_button">Login</string>
</resources>

<!-- res/values-es/strings.xml (Spanish) -->
<resources>
<string name="welcome_title">¡Bienvenido, %1$s!</string>
<string name="login_button">Iniciar sesión</string>
</resources>
// Usage in Compose
Text(stringResource(R.string.welcome_title, userName))
Text(stringResource(R.string.login_button))

Flutter - Localization

# pubspec.yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.18.0

flutter:
generate: true
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
// lib/l10n/app_en.arb
{
"welcomeTitle": "Welcome, {name}!",
"@welcomeTitle": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"loginButton": "Login"
}
// Usage
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Text(AppLocalizations.of(context)!.welcomeTitle(userName))
Text(AppLocalizations.of(context)!.loginButton)

Summary

UI Standards Best Practices:

  1. Define design tokens for consistency
  2. Build reusable component library
  3. Support accessibility with proper labels and touch targets
  4. Use semantic colors for dark mode support
  5. Implement localization from the start
  6. Support dynamic type/font scaling
  7. Test with accessibility tools (VoiceOver, TalkBack, etc.)
AspectiOSAndroidFlutter
Design TokensEnums/StructsObject/ConstantsClasses
AccessibilityVoiceOverTalkBackSemantics widget
Dark ModeAsset catalogsColor schemesThemeData
Localization.strings filesstrings.xmlARB files