design-to-flutter
SKILL.md
Design to Flutter
Convert design component specifications to Flutter widgets.
Overview
This skill transforms design specifications into idiomatic Flutter widgets with proper theming, composition patterns, and Material/Cupertino compliance.
This skill covers:
- Converting design frames to Flutter widget trees
- Mapping design properties to widget parameters
- Generating theme-aware widgets
- Creating widget tests
This skill does NOT cover:
- Design token extraction (see
design-tokens-extractionskill) - State management (Provider, Riverpod, Bloc)
- Navigation and routing
Quick Reference
Design to Flutter Mapping
| Design Element | Flutter Widget |
|---|---|
| Frame/Group | Container, SizedBox |
| Auto-layout horizontal | Row |
| Auto-layout vertical | Column |
| Overlapping layers | Stack |
| Text | Text |
| Rectangle | Container with BoxDecoration |
| Circle | Container with BoxShape.circle |
| Image | Image.asset / Image.network |
| Scroll | SingleChildScrollView, ListView |
| Grid | GridView |
Layout Properties
| Design Property | Flutter Equivalent |
|---|---|
| Padding | Padding widget or padding parameter |
| Gap/Spacing | SizedBox or MainAxisAlignment.spaceBetween |
| Alignment | MainAxisAlignment, CrossAxisAlignment |
| Fill container | Expanded, Flexible |
| Fixed size | SizedBox, Container with constraints |
Workflow: Convert Design Component
Step 1: Analyze Design Structure
Read the design component and identify:
- Widget hierarchy and nesting
- Layout direction and spacing
- Theme token usage
- Interactive elements
Step 2: Generate Widget Code
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class ComponentName extends StatelessWidget {
final String title;
final String? subtitle;
final VoidCallback? onTap;
const ComponentName({
super.key,
required this.title,
this.subtitle,
this.onTap,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(AppTheme.spacing.md),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radius.md),
boxShadow: [AppTheme.shadows.sm],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyLarge,
),
if (subtitle != null) ...[
SizedBox(height: AppTheme.spacing.xs),
Text(
subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
),
),
);
}
}
Step 3: Generate Theme Extensions
import 'package:flutter/material.dart';
class AppTheme {
// Spacing scale
static const spacing = _Spacing();
// Border radius
static const radius = _Radius();
// Shadows
static const shadows = _Shadows();
// Light theme
static ThemeData get light => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
brightness: Brightness.light,
),
textTheme: _textTheme,
);
// Dark theme
static ThemeData get dark => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
brightness: Brightness.dark,
),
textTheme: _textTheme,
);
static const _textTheme = TextTheme(
displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.w400),
displayMedium: TextStyle(fontSize: 45, fontWeight: FontWeight.w400),
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w600),
headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
labelLarge: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
);
}
class _Spacing {
const _Spacing();
double get xs => 4;
double get sm => 8;
double get md => 16;
double get lg => 24;
double get xl => 32;
}
class _Radius {
const _Radius();
double get sm => 4;
double get md => 8;
double get lg => 16;
double get full => 9999;
}
class _Shadows {
const _Shadows();
BoxShadow get sm => BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
);
BoxShadow get md => BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 4),
);
BoxShadow get lg => BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 10),
);
}
Common Patterns
Card Widget
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final VoidCallback? onTap;
const AppCard({
super.key,
required this.child,
this.padding,
this.onTap,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget card = Container(
padding: padding ?? EdgeInsets.all(AppTheme.spacing.md),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radius.lg),
boxShadow: [AppTheme.shadows.md],
),
child: child,
);
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppTheme.radius.lg),
child: card,
);
}
return card;
}
}
Button Widget
enum ButtonVariant { primary, secondary, ghost }
enum ButtonSize { small, medium, large }
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final ButtonVariant variant;
final ButtonSize size;
final IconData? leftIcon;
final IconData? rightIcon;
final bool isLoading;
const AppButton({
super.key,
required this.label,
this.onPressed,
this.variant = ButtonVariant.primary,
this.size = ButtonSize.medium,
this.leftIcon,
this.rightIcon,
this.isLoading = false,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
final padding = switch (size) {
ButtonSize.small => EdgeInsets.symmetric(horizontal: 12, vertical: 8),
ButtonSize.medium => EdgeInsets.symmetric(horizontal: 16, vertical: 12),
ButtonSize.large => EdgeInsets.symmetric(horizontal: 24, vertical: 16),
};
final textStyle = switch (size) {
ButtonSize.small => theme.textTheme.labelSmall,
ButtonSize.medium => theme.textTheme.labelLarge,
ButtonSize.large => theme.textTheme.labelLarge?.copyWith(fontSize: 16),
};
final (backgroundColor, foregroundColor) = switch (variant) {
ButtonVariant.primary => (
theme.colorScheme.primary,
theme.colorScheme.onPrimary,
),
ButtonVariant.secondary => (
theme.colorScheme.secondaryContainer,
theme.colorScheme.onSecondaryContainer,
),
ButtonVariant.ghost => (
Colors.transparent,
theme.colorScheme.primary,
),
};
return Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(AppTheme.radius.md),
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppTheme.radius.md),
child: Padding(
padding: padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isLoading)
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: foregroundColor,
),
)
else if (leftIcon != null)
Icon(leftIcon, size: 20, color: foregroundColor),
if (leftIcon != null || isLoading) SizedBox(width: 8),
Text(label, style: textStyle?.copyWith(color: foregroundColor)),
if (rightIcon != null) ...[
SizedBox(width: 8),
Icon(rightIcon, size: 20, color: foregroundColor),
],
],
),
),
),
);
}
}
List Item Widget
class ListItem extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
const ListItem({
super.key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.onTap,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: AppTheme.spacing.md,
vertical: AppTheme.spacing.sm,
),
child: Row(
children: [
if (leading != null) ...[
leading!,
SizedBox(width: AppTheme.spacing.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.bodyLarge),
if (subtitle != null)
Text(
subtitle!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
if (trailing != null) ...[
SizedBox(width: AppTheme.spacing.md),
trailing!,
],
],
),
),
);
}
}
Widget Test Template
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/widgets/component_name.dart';
void main() {
group('ComponentName', () {
testWidgets('renders title', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: ComponentName(title: 'Test Title'),
),
),
);
expect(find.text('Test Title'), findsOneWidget);
});
testWidgets('renders subtitle when provided', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: ComponentName(
title: 'Title',
subtitle: 'Subtitle',
),
),
),
);
expect(find.text('Subtitle'), findsOneWidget);
});
testWidgets('calls onTap when tapped', (tester) async {
var tapped = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ComponentName(
title: 'Title',
onTap: () => tapped = true,
),
),
),
);
await tester.tap(find.byType(ComponentName));
expect(tapped, isTrue);
});
});
}
File Structure
lib/
├── theme/
│ ├── app_theme.dart
│ ├── colors.dart
│ └── typography.dart
├── widgets/
│ ├── app_button.dart
│ ├── app_card.dart
│ ├── list_item.dart
│ └── widgets.dart // barrel export
└── main.dart
See Also
design-tokens-extractionskill - Extract tokens firstdesign-token-jsonstyle - Token format referencecomponent-specstyle - Documentation format
Weekly Installs
1
Repository
arustydev/aiGitHub Stars
6
First Seen
7 days ago
Security Audits
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1