Agents design-to-flutter
Convert design specifications from Figma, Sketch, or Penpot to Flutter widgets with proper theming
install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/plugins/frontend/design-to-code/skills/design-to-flutter" ~/.claude/skills/arustydev-agents-design-to-flutter && rm -rf "$T"
manifest:
content/plugins/frontend/design-to-code/skills/design-to-flutter/SKILL.mdsource content
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
skill)design-tokens-extraction - State management (Provider, Riverpod, Bloc)
- Navigation and routing
Quick Reference
Design to Flutter Mapping
| Design Element | Flutter Widget |
|---|---|
| Frame/Group | , |
| Auto-layout horizontal | |
| Auto-layout vertical | |
| Overlapping layers | |
| Text | |
| Rectangle | with |
| Circle | with |
| Image | / |
| Scroll | , |
| Grid | |
Layout Properties
| Design Property | Flutter Equivalent |
|---|---|
| Padding | widget or parameter |
| Gap/Spacing | or |
| Alignment | , |
| Fill container | , |
| Fixed size | , 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
skill - Extract tokens firstdesign-tokens-extraction
style - Token format referencedesign-token-json
style - Documentation formatcomponent-spec