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
- Touch Targets: Minimum 44x44 points (iOS) / 48dp (Android)
- Contrast Ratios: WCAG AA minimum (4.5:1 for normal text, 3:1 for large text)
- Accessible Labels: All interactive elements must have labels
- Dynamic Type/Font Scaling: Support system font size preferences
- 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:
- Define design tokens for consistency
- Build reusable component library
- Support accessibility with proper labels and touch targets
- Use semantic colors for dark mode support
- Implement localization from the start
- Support dynamic type/font scaling
- Test with accessibility tools (VoiceOver, TalkBack, etc.)
| Aspect | iOS | Android | Flutter |
|---|---|---|---|
| Design Tokens | Enums/Structs | Object/Constants | Classes |
| Accessibility | VoiceOver | TalkBack | Semantics widget |
| Dark Mode | Asset catalogs | Color schemes | ThemeData |
| Localization | .strings files | strings.xml | ARB files |