create-widget

SKILL.md

Creating New Widgets

Step-by-step guide to creating widgets that follow the design system's architecture and conventions.

File Naming Conventions

Widget Files

Pattern: {widget_name}_widget.dart

// ✅ Good
scaffold_widget.dart
button_widget.dart
cupertino_text_field_widget.dart

// ❌ Avoid
scaffold.dart
btn_widget.dart
textfield.dart

Class Naming

Pattern: {Name}Widget

class ScaffoldWidget extends StatelessWidget {}
class ButtonWidget extends StatelessWidget {}
class IconWidget extends StatelessWidget {}

Widget Template

Basic StatelessWidget

// lib/src/widgets/my_custom_widget.dart
import 'package:flutter/cupertino.dart';
import '../../ios_design_system.dart';

/// iOS-style custom widget for [purpose].
///
/// This widget provides [main features] with automatic theme adaptation
/// for both light and dark modes.
///
/// ## Example
///
/// ```dart
/// MyCustomWidget(
///   title: 'Example',
///   subtitle: 'Description',
///   onPressed: () {},
/// )
/// ```
///
/// See also:
///
///  * [RelatedWidget], which provides similar functionality.
class MyCustomWidget extends StatelessWidget {
  /// Creates a custom widget.
  ///
  /// The [title] parameter is required and must not be null.
  const MyCustomWidget({
    required this.title,
    this.subtitle,
    this.onPressed,
    super.key,
  });

  /// The primary title text.
  final String title;

  /// Optional subtitle text shown below the title.
  final String? subtitle;

  /// Called when the widget is tapped.
  ///
  /// If null, the widget will be non-interactive.
  final VoidCallback? onPressed;

  
  Widget build(BuildContext context) {
    final theme = IosTheme.of(context);

    return CupertinoButtonWidget(
      onPressed: onPressed,
      color: switch (theme) {
        IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
        IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
      },
      borderRadius: BorderRadius.circular(12),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: theme.typography.bodyBold.copyWith(
              color: theme.defaultLabelColors.primary,
            ),
          ),
          if (subtitle != null) ...[
            const SizedBox(height: 4),
            Text(
              subtitle!,
              style: theme.typography.caption1Regular.copyWith(
                color: theme.defaultLabelColors.secondary,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

HookWidget Template

import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../../ios_design_system.dart';

class MyHookWidget extends HookWidget {
  const MyHookWidget({
    required this.initialValue,
    super.key,
  });

  final String initialValue;

  
  Widget build(BuildContext context) {
    final theme = IosTheme.of(context);
    final controller = useTextEditingController(text: initialValue);
    final isValid = useState(false);

    useEffect(() {
      void validate() {
        isValid.value = controller.text.isNotEmpty;
      }

      controller.addListener(validate);
      return () => controller.removeListener(validate);
    }, [controller]);

    return CupertinoTextFieldWidget(
      controller: controller,
      placeholder: 'Enter text',
    );
  }
}

Widget Variants Pattern

Factory Constructors

class MyWidget extends StatelessWidget {
  const MyWidget._internal({
    required this.variant,
    required this.content,
    super.key,
  });

  /// Standard variant with default styling
  factory MyWidget.standard({
    required String content,
  }) => MyWidget._internal(
    variant: MyWidgetVariant.standard,
    content: content,
  );

  /// Rounded variant with 14pt border radius
  factory MyWidget.rounded({
    required String content,
  }) => MyWidget._internal(
    variant: MyWidgetVariant.rounded,
    content: content,
  );

  /// Stocks variant with gradient background
  factory MyWidget.stocks({
    required String content,
  }) => MyWidget._internal(
    variant: MyWidgetVariant.stocks,
    content: content,
  );

  final MyWidgetVariant variant;
  final String content;

  
  Widget build(BuildContext context) {
    final theme = IosTheme.of(context);

    return Container(
      decoration: switch (variant) {
        MyWidgetVariant.standard => BoxDecoration(
          color: theme.defaultSystemBackgroundsColors.primaryLight,
        ),
        MyWidgetVariant.rounded => BoxDecoration(
          color: theme.defaultSystemBackgroundsColors.primaryLight,
          borderRadius: BorderRadius.circular(14),
        ),
        MyWidgetVariant.stocks => theme.stocksDecorations.gradients.background,
      },
      child: Text(content),
    );
  }
}

enum MyWidgetVariant {
  standard,
  rounded,
  stocks,
}

Theme Integration

Required Pattern

ALWAYS access theme via context:


Widget build(BuildContext context) {
  final theme = IosTheme.of(context);

  // Extract colors
  final backgroundColor = switch (theme) {
    IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
    IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
  };

  final textColor = theme.defaultLabelColors.primary;
  final textStyle = theme.typography.bodyRegular;

  return Container(
    color: backgroundColor,
    child: Text(
      'Text',
      style: textStyle.copyWith(color: textColor),
    ),
  );
}

Theme Callbacks

For dynamic theme access in widget parameters:

class MyWidget extends StatelessWidget {
  const MyWidget({
    required this.title,
    this.backgroundColorCallback,
    this.textColorCallback,
    super.key,
  });

  final String title;
  final Color Function(IosThemeData)? backgroundColorCallback;
  final Color Function(IosThemeData)? textColorCallback;

  
  Widget build(BuildContext context) {
    final theme = IosTheme.of(context);

    final backgroundColor = backgroundColorCallback?.call(theme) ??
      theme.defaultSystemBackgroundsColors.primaryLight;

    final textColor = textColorCallback?.call(theme) ??
      theme.defaultLabelColors.primary;

    return Container(
      color: backgroundColor,
      child: Text(
        title,
        style: theme.typography.bodyRegular.copyWith(color: textColor),
      ),
    );
  }
}

// Usage
MyWidget(
  title: 'Hello',
  backgroundColorCallback: (theme) => theme.defaultColors.systemBlue,
  textColorCallback: (theme) => theme.defaultColors.systemWhite,
)

Sealed Classes for Type Safety

Button Color Example

sealed class ButtonColor {
  const ButtonColor();

  Color? backgroundEnabled({required BuildContext context});
  Color? backgroundDisabled({required BuildContext context});
  Color labelEnabled({required BuildContext context});
  Color labelDisabled({required BuildContext context});
  Gradient? backgroundGradientEnabled({required BuildContext context}) => null;
}

class BlueButtonColor extends ButtonColor {
  const BlueButtonColor();

  
  Color? backgroundEnabled({required BuildContext context}) {
    final theme = IosTheme.of(context);
    return theme.defaultColors.systemBlue;
  }

  
  Color? backgroundDisabled({required BuildContext context}) {
    final theme = IosTheme.of(context);
    return theme.defaultFillColors.secondary;
  }

  
  Color labelEnabled({required BuildContext context}) {
    final theme = IosTheme.of(context);
    return theme.defaultColors.systemWhite;
  }

  
  Color labelDisabled({required BuildContext context}) {
    final theme = IosTheme.of(context);
    return theme.defaultLabelColors.tertiary;
  }
}

class CustomButtonColor extends ButtonColor {
  const CustomButtonColor({
    required this.backgroundEnabledColor,
    required this.backgroundDisabledColor,
    required this.labelEnabledColor,
    required this.labelDisabledColor,
    this.backgroundGradientEnabledValue,
  });

  final Color? backgroundEnabledColor;
  final Color? backgroundDisabledColor;
  final Color labelEnabledColor;
  final Color? labelDisabledColor;
  final Gradient? backgroundGradientEnabledValue;

  
  Color? backgroundEnabled({required BuildContext context}) =>
    backgroundEnabledColor;

  
  Color? backgroundDisabled({required BuildContext context}) =>
    backgroundDisabledColor;

  
  Color labelEnabled({required BuildContext context}) =>
    labelEnabledColor;

  
  Color labelDisabled({required BuildContext context}) =>
    labelDisabledColor ?? labelEnabledColor.withOpacity(0.3);

  
  Gradient? backgroundGradientEnabled({required BuildContext context}) =>
    backgroundGradientEnabledValue;
}

Export Strategy

Step 1: Create Widget File

Place in /lib/src/widgets/:

/lib/src/widgets/my_custom_widget.dart

Step 2: Export Widget

Add to /lib/src/widgets/exports.dart:

export 'my_custom_widget.dart';

Step 3: Import in Code

import 'package:ios_design_system/ios_design_system.dart';

// Now you can use
MyCustomWidget(title: 'Hello')

Apple HIG Compliance

Spacing Standards

// Use standard spacing multiples
const EdgeInsets.all(8)    // Micro spacing
const EdgeInsets.all(16)   // Standard spacing
const EdgeInsets.all(24)   // Section spacing
const EdgeInsets.all(32)   // Large spacing

const SizedBox(height: 8)   // Small gaps
const SizedBox(height: 16)  // Standard gaps
const SizedBox(height: 24)  // Section gaps
const SizedBox(width: 4)    // Between tags
const SizedBox(width: 8)    // Between buttons

Border Radius Standards

BorderRadius.circular(7)    // Icon backgrounds
BorderRadius.circular(10)   // Search fields
BorderRadius.circular(12)   // Large buttons, cards
BorderRadius.circular(14)   // Grouped tables, small buttons
BorderRadius.circular(50)   // Pills, tags, medium buttons

Touch Targets

Minimum: 44pt x 44pt

BoxConstraints(
  minHeight: 44,
  minWidth: 44,
)

Typography

// Use theme typography
theme.typography.largeTitleBold    // 34pt - Page headers
theme.typography.title1Bold        // 28pt - Section titles
theme.typography.title2Bold        // 22pt - Subsection titles
theme.typography.title3Bold        // 20pt - Card titles
theme.typography.bodyRegular       // 17pt - Body text (DEFAULT)
theme.typography.calloutRegular    // 16pt - Secondary content
theme.typography.subheadlineRegular // 15pt - Small buttons
theme.typography.footnoteRegular   // 13pt - Footnotes
theme.typography.caption1Regular   // 12pt - Captions
theme.typography.caption2Regular   // 11pt - Smallest text

Testing Checklist

Create Test File

// test/widgets/my_custom_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:ios_design_system/ios_design_system.dart';

void main() {
  testWidgets('MyCustomWidget displays title', (tester) async {
    await tester.pumpWidget(
      IosAnimatedTheme(
        data: IosLightThemeData(),
        child: CupertinoApp(
          home: MyCustomWidget(
            title: 'Test Title',
          ),
        ),
      ),
    );

    expect(find.text('Test Title'), findsOneWidget);
  });

  testWidgets('MyCustomWidget adapts to dark theme', (tester) async {
    await tester.pumpWidget(
      IosAnimatedTheme(
        data: IosDarkThemeData(),
        child: CupertinoApp(
          home: MyCustomWidget(
            title: 'Test',
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();
    // Test dark theme specific behavior
  });

  testWidgets('MyCustomWidget handles null subtitle', (tester) async {
    await tester.pumpWidget(
      IosAnimatedTheme(
        data: IosLightThemeData(),
        child: CupertinoApp(
          home: MyCustomWidget(
            title: 'Title',
            subtitle: null,
          ),
        ),
      ),
    );

    expect(find.text('Title'), findsOneWidget);
  });
}

Quality Checklist

Before committing your widget:

  • Widget file named with _widget.dart suffix
  • Class named with Widget suffix
  • Exported in /lib/src/widgets/exports.dart
  • Uses IosTheme.of(context) for all visual properties
  • NO hardcoded colors (uses theme colors)
  • NO hardcoded text styles (uses theme typography)
  • Supports both light and dark modes
  • Follows Apple HIG spacing (8pt, 16pt, 24pt, 32pt)
  • Follows Apple HIG sizing (min 44pt touch targets)
  • Uses sealed classes for variants (if applicable)
  • Includes dartdoc comments
  • Has example usage in comments
  • Created unit tests
  • Tested in light mode
  • Tested in dark mode
  • Tested with different text scales
  • Uses const constructors where possible
  • Disposes controllers properly (if stateful)
  • Added to example app

Common Mistakes to Avoid

❌ Don't Hardcode Colors

// ❌ Bad
Container(
  color: Color(0xFF007AFF),
  child: child,
)

// ✅ Good
Container(
  color: theme.defaultColors.systemBlue,
  child: child,
)

❌ Don't Hardcode Text Styles

// ❌ Bad
Text(
  'Hello',
  style: TextStyle(fontSize: 17, fontWeight: FontWeight.w400),
)

// ✅ Good
Text(
  'Hello',
  style: theme.typography.bodyRegular,
)

❌ Don't Ignore Dark Mode

// ❌ Bad - only works in light mode
Container(
  color: theme.defaultSystemBackgroundsColors.primaryLight,
  child: child,
)

// ✅ Good - adapts to theme
Container(
  color: switch (theme) {
    IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
    IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
  },
  child: child,
)

❌ Don't Use Material Widgets

// ❌ Bad
import 'package:flutter/material.dart';

Scaffold(
  appBar: AppBar(title: Text('Title')),
  body: child,
)

// ✅ Good
import 'package:flutter/cupertino.dart';

ScaffoldWidget(
  navigationBar: CupertinoNavigatorBarWidget(
    title: 'Title',
  ),
  child: child,
)

❌ Don't Create State Classes for Simple State

// ❌ Bad - too much boilerplate
class MyWidget extends StatefulWidget {
  
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool _isEnabled = false;

  
  Widget build(BuildContext context) {
    return SwitchWidget(
      value: _isEnabled,
      onChanged: (value) => setState(() => _isEnabled = value),
    );
  }
}

// ✅ Good - use hooks
class MyWidget extends HookWidget {
  const MyWidget({super.key});

  
  Widget build(BuildContext context) {
    final isEnabled = useState(false);

    return SwitchWidget(
      value: isEnabled.value,
      onChanged: (value) => isEnabled.value = value,
    );
  }
}

Example: Creating a Card Widget

Complete example from start to finish:

1. Create File

/lib/src/widgets/custom_card_widget.dart

2. Write Widget

import 'package:flutter/cupertino.dart';
import '../../ios_design_system.dart';

/// iOS-style card widget with rounded corners and shadow.
///
/// Displays content in a card format with automatic theme adaptation.
///
/// ## Example
///
/// ```dart
/// CustomCardWidget(
///   title: 'Card Title',
///   description: 'Card description text',
///   icon: CupertinoIcons.star,
///   onPressed: () {},
/// )
/// ```
class CustomCardWidget extends StatelessWidget {
  const CustomCardWidget({
    required this.title,
    this.description,
    this.icon,
    this.onPressed,
    super.key,
  });

  final String title;
  final String? description;
  final IconData? icon;
  final VoidCallback? onPressed;

  
  Widget build(BuildContext context) {
    final theme = IosTheme.of(context);

    return CupertinoButtonWidget(
      onPressed: onPressed,
      padding: EdgeInsets.zero,
      color: Colors.transparent,
      child: Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: switch (theme) {
            IosLightThemeData() =>
              theme.defaultSystemBackgroundsColors.secondaryLight,
            IosDarkThemeData() =>
              theme.defaultSystemBackgroundsColors.secondaryDarkElevated,
          },
          borderRadius: BorderRadius.circular(12),
        ),
        child: Row(
          children: [
            if (icon != null) ...[
              IconWidget.background(
                iconData: icon!,
                backgroundColorCallback: (theme) =>
                  theme.defaultColors.systemBlue,
              ),
              const SizedBox(width: 16),
            ],
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: theme.typography.bodyBold.copyWith(
                      color: theme.defaultLabelColors.primary,
                    ),
                  ),
                  if (description != null) ...[
                    const SizedBox(height: 4),
                    Text(
                      description!,
                      style: theme.typography.caption1Regular.copyWith(
                        color: theme.defaultLabelColors.secondary,
                      ),
                    ),
                  ],
                ],
              ),
            ),
            if (onPressed != null)
              Icon(
                CupertinoIcons.chevron_right,
                color: theme.defaultLabelColors.tertiary,
                size: 20,
              ),
          ],
        ),
      ),
    );
  }
}

3. Export Widget

Add to /lib/src/widgets/exports.dart:

export 'custom_card_widget.dart';

4. Create Test

// test/widgets/custom_card_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:ios_design_system/ios_design_system.dart';

void main() {
  testWidgets('CustomCardWidget displays title', (tester) async {
    await tester.pumpWidget(
      IosAnimatedTheme(
        data: IosLightThemeData(),
        child: CupertinoApp(
          home: CustomCardWidget(
            title: 'Test Card',
          ),
        ),
      ),
    );

    expect(find.text('Test Card'), findsOneWidget);
  });
}

5. Add to Example App

// example/lib/main.dart
CustomCardWidget(
  title: 'Example Card',
  description: 'This is a card widget',
  icon: CupertinoIcons.star,
  onPressed: () {},
)

Quick Reference

Must Do

  • ✅ Use IosTheme.of(context)
  • ✅ Test light AND dark modes
  • ✅ Follow Apple HIG spacing
  • ✅ Use const constructors
  • ✅ Export in exports.dart
  • ✅ Add dartdoc comments

Never Do

  • ❌ Hardcode colors
  • ❌ Hardcode text styles
  • ❌ Use Material widgets
  • ❌ Ignore dark mode
  • ❌ Skip testing
  • ❌ Forget to export
Weekly Installs
1
GitHub Stars
18
First Seen
Mar 2, 2026
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1