design-to-flutter
Convert design specifications from Figma, Sketch, or Penpot to Flutter widgets with proper theming
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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,
});
@override
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,
});
@override
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,
});
@override
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,
});
@override
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 first -
design-token-jsonstyle - Token format reference -
component-specstyle - Documentation format
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-03-11
- Index updated
- 2026-03-11
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!