modelscope.cn

design-to-flutter

Convert design specifications from Figma, Sketch, or Penpot to Flutter widgets with proper theming

Installation

$ npx skills add https://modelscope.cn

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from modelscope.cn · top by installs.

npx skills add https://modelscope.cn

Browse all from modelscope.cn

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,271 B

History

  1. First recorded snapshot · 0 installs

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-extraction skill)
  • 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-extraction skill - Extract tokens first
  • design-token-json style - Token format reference
  • component-spec style - Documentation format