Skip to content

☂️ Reinforcement: Add more basic components to the core framework #97496

Description

@Hawmex

This issue tracks proposals and development for net-new widgets and functionalities to be added to the core widgets library.

This effort is the other side of the coin to our reinforcement work, see the refactoring side as well. While the refactoring issue focuses on moving existing code, this issue is dedicated to identifying and building new, foundational, style-agnostic primitives that are missing from the core widgets library today. The goal is to fill gaps in the core and make it an even more powerful foundation for building any design system.

Note

Some of these may be best informed by a UXR study. Before considering contributing one of these net-new widgets, reach out to the team on Discord to confirm.

Propose a New Core Widget! 💡

What foundational widget or capability do you wish existed in Flutter's core? If you have an idea for a new, un-opinionated "headless" widget that would be broadly useful, this is the place to propose it. Please provide a clear use case and rationale for why it belongs in the core framework.

Last update Mar 4, 2026

So far, we have investigated many of the existing design components to determine if any refactoring can be done before decoupling, moving existing APIs to the widgets layer to a common surface (which is tracked in #53059). In some cases, we have decided not to break widgets in the design libraries to try to accomplish this, which leaves the opportunity for new, more generic APIs to be introduced in the widgets layer later.

As we have been focused on the opportunity to refactor existing widgets (#53059) ahead of actually separating the libraries, expect this work of adding new widgets to continue after the design libraries land in their new packages, which is what this issue is meant to track.

To check out all the issues and aspects of the decoupling effort, see the github project, and the root parent issue: #101479.

Status:

Raw- widgets that pre-date this effort

This is not an exhaustive list of components available in the widgets library, but specifically those with Raw prefixes. 🙂


OP Hi, It's been 2 months since I've ditched HTML, CSS, and JS for Flutter and I haven't looked back. I love the declarative style of Flutter and Dart itself.

What's the problem?

What really bothers me is I find it hard to implement completely Custom UIs. I mean even when I wanted to use a custom font, I had to depend on material.dart and use Scaffold to make it work. I don't even want to start talking about the frustrations of theming a material app in flutter. But this was not a problem using HTML, CSS, JS. They basically give you simple and minimal elements and then let you do whatever you want with them or use whatever UI package you want, such as Tailwind, MUI, etc.
So, It's either a matter of Flutter lacking documentation on Custom UIs or me not reading enough docs about them.

Any other problems?

Well, if you just take a look at the reported issues, they're mostly about material.dart and cupertino.dart. I feel like Flutter Team is spending most of their time, effort and energy on these two UIs instead of the framework itself. And still, Flutter is far behind from Material 3 spec (and even Material 2, because material.dart uses 2014 typography by default.).

My Ideal Flutter

So my ideal is:

  • The main Flutter package should contain and export basic widgets of the framework (GestureDetector, StatelessWidget, StatefulWidget, etc.). These can be currently found in widgets.dart. This way, Flutter Team's efforts are concentrated on the framework itself.
  • material and cupertino should come in seperate packages (and export the main Flutter package) and be maintained by Flutter community more.
  • More tutorials and docs should be provided on making UIs with the main Flutter package rather than making them with material or cupertino. This way more people will know how underlying widgets of material and cupertino work and can take part in maintaining them or even make their own UIs in their apps and packages.

This way we will be able to see more Flutter UI packages and a more diverse set of Flutter apps in terms of UI on the market.

After all, these are just my ideals and may be far from reality but I really like to see some discussion on this matter and finding the best solution to it.

Activity

  1. HerrNiklasRaab commented on Jan 30, 2022

    @HerrNiklasRaab
  2. darshankawar commented on Jan 31, 2022

    @darshankawar
    Member

    @Hawmex
    Thanks for the report. Implementing custom UIs is probably a broad topic. Can you give us a real world example where custom UI you proposed would be helpful rather than using the current implementation ?

  3. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 31, 2022
  4. Hawmex commented on Jan 31, 2022

    @Hawmex
    Author

    Well, decoupling material.dart from Flutter has been also discussed in #91605 and many reasons have somehow been ignored.

    FWIW, the main reason to not have material be its own package is that we want the out-of-box experience to be quick. That is, people should be able to download Flutter, and immediately start writing an app with pretty buttons.

    Yeah, but Flutter kind of already does that for icons and it works, right? There is uses-material-design: true and cupertino_icons in every app (because of the template) and you don't see people complaining it is hard to add material icons to Flutter because most use the template.

    But the reason for implementing custom UIs is in my opinion an obvious thing. Not all developers and companies want to follow Cupertino or Material design guidelines. And even in their UI architecture, they may want to follow different architecture patterns from material.dart (for example theming, scaffolding, etc.).

    I live in Iran and I'd like to use Persian fonts rather than the default Roboto. I followed the documentation but it didn't work. Turned out I had to wrap my page in Scaffold, but it wasn't mentioned in the docs.

    Some other reasons:

    • Even in material.dart itself, there are inconsistencies in coding patterns. Opening dialogs is handled through a top-level function while showing a snackbar is handled through an InheritedWidget pattern.
    • Flutter has always had to avoid breaking changes in material.dart and go on with accumulating old stuff and deprecating them for God knows how long. Just take a look at how many parameters ThemeData has as of today.
    • material.dart has been integrated with the framework itself that you can't make Flutter apps without it. A fine example would be the urge to use Scaffold to load custom fonts in the current theming pattern.

    Just decouple material.dart and cupertino.dart from Flutter and give people the option to make their own UI or choose it from external packages including Material and Cupertino. And I'm sure those who love material design would continue to use it in a separate package.

  5. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jan 31, 2022
  6. darshankawar commented on Feb 1, 2022

    @darshankawar
    Member

    Keeping it open and labeling as a proposal for further inputs from the team.

    /cc @HansMuller

  7. added
    p: material_uimaterial_ui package in flutter/packages
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    and removed
    in triagePresently being triaged by the triage team
    on Feb 1, 2022
  8. darshankawar commented on Feb 1, 2022

    @darshankawar
    Member

    I just found out this similar / related issue: flutter/website#7763
    @Hawmex Please confirm if your proposal can be tracked in the linked issue.

  9. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Feb 1, 2022
  10. Hawmex commented on Feb 2, 2022

    @Hawmex
    Author

    @darshankawar
    About Providing documentation on the undrlying Flutter framework widgets, yes. It can be tracked there.

    But I also propose getting material.dart and cupertino.dart out of Flutter and providing them as separate packages.

  11. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Feb 2, 2022
  12. 34 remaining items

  13. Piinks commented on Mar 5, 2026

    @Piinks
    Contributor

    Hi everyone!
    We've been so heads down on this work, if you haven't been keeping up with the individual issues, pull requests, and design documents, I thought an update was needed here. :)

    This issue represents one of the 4 pillars of this effort (see parent issue), adding new basic components to the core framework.

    During this period that the libraries are still together, we've been primarily focused on refactoring existing design widgets to those basic components where we can (tracked in #53059), but as a result, we already have some new components in the widgets layer! I have updated the OP to cover a lot of the work that has been done in relation to this pillar.

    Once we land the libraries in their new packages (hello, material_ui and cupertino_ui 👋 ), we'll continue to work on this pillar of the project to add more to the core. Thanks all, keep the feedback coming!

  14. Hannnes1 commented on Mar 9, 2026

    @Hannnes1
    Contributor

    Regarding the latest update, and the point about buttons.

    One thing I always struggle with when implementing custom buttons is the various states (active, disabled, hovered, pressed, etc.). What complicates is further is when the states interact with each other; a button can be disabled and hovered, active and pressed, etc.

    I'm not sure if it's even possible for a design agnostic widget, but it would simplify creating buttons a lot.

    I think Hixies original blankcanvas design doc had some discussions about buttons, but I can't find that now...

  15. iapicca commented on Mar 9, 2026

    @iapicca

    I think Hixies original blankcanvas design doc had some discussions about buttons, but I can't find that now...

    @Hannnes1 blankcanvas mentioned! <3
    Here is the link to the document and you can find the discussion below

  16. kerberjg commented on Mar 9, 2026

    @kerberjg
    Contributor

    @Hannnes1 Correct! Eg. one often overlooked state for the iOS buttons is a loading state (button becomes disabled and shows a spinner, awaiting a Future to complete)

    API proposal: what if the button had a an abstract base class with a generic type for a "state"? That could be an enum/string/int, evaluated like this:

    abstract class BaseButton<S> {
      ...
      Widget buildState(S state) {
        ...
      }
      ...
    }
    
    enum CustomButtonState { idle, disabled, loading, dancing };
    
    class CustomButton extends BaseButton<CustomButtonState> { ... }
  17. Pante commented on Mar 9, 2026

    @Pante
    Contributor

    One thing I always struggle with when implementing custom buttons is the various states (active, disabled, hovered, pressed, etc.). What complicates is further is when the states interact with each other; a button can be disabled and hovered, active and pressed, etc.

    I'm not sure if it's even possible for a design agnostic widget, but it would simplify creating buttons a lot.

    We faced a similar issue when designing Forui's styling system & I do think it's definitely possible using an enhanced WidgetStateMap similar to what is outlined in our design docs for it.

  18. davidhicks980 commented on Mar 10, 2026

    @davidhicks980
    Contributor

    @Hannnes1 I've had luck using an InheritedNotifier that passes down widget state.

    For example, here's a Button that only defines interactions:

    Dartpad: https://dartpad.dev/45ab05be46c7a72a2ee59d52fba1d6a8

    (also, I just wrote Button as an example, so make sure to test your own version if you use this approach)

    class ButtonExample extends StatelessWidget {
      const ButtonExample({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Button(
            onPressed: () {
              ScaffoldMessenger.of(
                context,
              ).showSnackBar(const SnackBar(content: Text('Button Pressed!')));
            },
            behavior: HitTestBehavior.opaque,
            mouseCursor: WidgetStateProperty.all(SystemMouseCursors.click),
            child: Builder(
              builder: (context) {
                final ButtonState state = Button.of(context)!;
                final bool isHovered = state.hovered;
                final bool isPressed = state.pressed;
                final bool isFocused = state.focused;
                return AnimatedContainer(
                  duration: const Duration(milliseconds: 50),
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                  decoration: BoxDecoration(
                    color: isPressed
                        ? Colors.blue.shade700
                        : isHovered
                        ? Colors.blue.shade600
                        : Colors.blue.shade500,
                    borderRadius: BorderRadius.circular(8),
                    border: isFocused ? Border.all(color: Colors.yellow, width: 2) : null,
                  ),
                  child: const Text('Button', style: TextStyle(color: Colors.white, fontSize: 16)),
                );
              },
            ),
          ),
        );
      }
    }
    Button
    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/semantics.dart';
    
    class _ButtonStateScope extends InheritedNotifier<WidgetStatesController> {
      const _ButtonStateScope({required super.notifier, required super.child});
    }
    
    extension type ButtonState._(Set<WidgetState> value) {
      bool get hovered => value.contains(WidgetState.hovered);
      bool get pressed => value.contains(WidgetState.pressed);
      bool get focused => value.contains(WidgetState.focused);
      bool get disabled => value.contains(WidgetState.disabled);
      Set<WidgetState> get states => value;
    }
    
    class Button extends StatefulWidget {
      const Button({
        super.key,
        this.onHover,
        this.onPressed,
        this.onFocusChange,
        this.focusNode,
        this.autofocus = false,
        this.behavior = HitTestBehavior.deferToChild,
        this.state,
        this.mouseCursor,
        required this.child,
      });
    
      final ValueChanged<bool>? onHover;
      final VoidCallback? onPressed;
      final ValueChanged<bool>? onFocusChange;
      final FocusNode? focusNode;
      final bool autofocus;
      final HitTestBehavior behavior;
      final Set<WidgetState>? state;
      final WidgetStateProperty<MouseCursor>? mouseCursor;
      final Widget child;
    
      static ButtonState of(BuildContext context) {
        final Set<WidgetState> value = context
            .dependOnInheritedWidgetOfExactType<_ButtonStateScope>()
            !.notifier
            !.value;
        return ButtonState._(value);
      }
    
      @override
      State<Button> createState() => _ButtonState();
    }
    
    class _ButtonState extends State<Button> {
      late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleActivation),
        ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(onInvoke: _handleActivation),
      };
      Map<Type, GestureRecognizerFactory>? _gestures;
      DeviceGestureSettings? _gestureSettings;
    
      // If a focus node isn't given to the widget, then we have to manage our own.
      FocusNode? _internalFocusNode;
      FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode!;
      final WidgetStatesController _statesController = WidgetStatesController();
    
      bool get isHovered => _isHovered;
      bool _isHovered = false;
      set isHovered(bool value) {
        if (_isHovered != value) {
          _isHovered = value;
          _statesController.update(WidgetState.hovered, value);
        }
      }
    
      bool get isPressed => _isPressed;
      bool _isPressed = false;
      set isPressed(bool value) {
        if (_isPressed != value) {
          _isPressed = value;
          _statesController.update(WidgetState.pressed, value);
        }
      }
    
      bool get isFocused => _isFocused;
      bool _isFocused = false;
      set isFocused(bool value) {
        if (_isFocused != value) {
          _isFocused = value;
          _statesController.update(WidgetState.focused, value);
        }
      }
    
      bool get isEnabled => _isEnabled;
      bool _isEnabled = false;
      set isEnabled(bool value) {
        if (_isEnabled != value) {
          _isEnabled = value;
          _statesController.update(WidgetState.disabled, !value);
        }
      }
    
      @override
      void initState() {
        super.initState();
        if (widget.focusNode == null) {
          _internalFocusNode = FocusNode();
        }
    
        _statesController.value = widget.state ?? <WidgetState>{};
        isEnabled = widget.onPressed != null;
        isFocused = _focusNode.hasPrimaryFocus;
      }
    
      @override
      void didUpdateWidget(Button oldWidget) {
        super.didUpdateWidget(oldWidget);
        if (widget.focusNode != oldWidget.focusNode) {
          if (widget.focusNode != null) {
            _internalFocusNode?.dispose();
            _internalFocusNode = null;
          } else {
            assert(_internalFocusNode == null);
            _internalFocusNode = FocusNode();
          }
    
          isFocused = _focusNode.hasPrimaryFocus;
        }
    
        if (widget.state != oldWidget.state && widget.state != null) {
          _statesController.value = widget.state!;
        }
    
        if (widget.onPressed != oldWidget.onPressed) {
          if (widget.onPressed == null) {
            isEnabled = isHovered = isPressed = isFocused = false;
          } else {
            isEnabled = true;
          }
        }
      }
    
      @override
      void dispose() {
        _statesController.dispose();
        _internalFocusNode?.dispose();
        _internalFocusNode = null;
        super.dispose();
      }
    
      void _handleFocusChange([bool? focused]) {
        isFocused = _focusNode.hasPrimaryFocus;
        widget.onFocusChange?.call(isFocused);
      }
    
      void _handleActivation([Intent? intent]) {
        isPressed = false;
        widget.onPressed?.call();
      }
    
      void _handleTapDown(TapDownDetails details) {
        isPressed = true;
      }
    
      void _handleTapUp(TapUpDetails? details) {
        isPressed = false;
        widget.onPressed?.call();
      }
    
      void _handleTapCancel() {
        isPressed = false;
      }
    
      void _handlePointerExit(PointerExitEvent event) {
        if (isHovered) {
          isHovered = false;
          widget.onHover?.call(false);
        }
      }
    
      void _handlePointerEnter(PointerEnterEvent event) {
        if (!isHovered) {
          isHovered = true;
          widget.onHover?.call(true);
        }
      }
    
      void _handleDismiss() {
        Actions.invoke(context, const DismissIntent());
      }
    
      @override
      Widget build(BuildContext context) {
        final DeviceGestureSettings? newGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
        if (_gestureSettings != newGestureSettings) {
          _gestureSettings = newGestureSettings;
          _gestures = null;
        }
    
        _gestures ??= <Type, GestureRecognizerFactory>{
          TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
            () => TapGestureRecognizer(debugOwner: this),
            (TapGestureRecognizer instance) {
              instance
                ..onTapDown = _handleTapDown
                ..onTapUp = _handleTapUp
                ..onTapCancel = _handleTapCancel
                ..gestureSettings = _gestureSettings;
            },
          ),
        };
    
        final child = RawGestureDetector(
          behavior: widget.behavior,
          gestures: isEnabled ? _gestures! : const <Type, GestureRecognizerFactory>{},
          child: widget.child,
        );
    
        return MergeSemantics(
          child: Semantics.fromProperties(
            properties: SemanticsProperties(
              enabled: isEnabled,
              onDismiss: isEnabled ? _handleDismiss : null,
            ),
            child: Actions(
              actions: isEnabled ? _actions : <Type, Action<Intent>>{},
              child: Focus(
                autofocus: isEnabled && widget.autofocus,
                focusNode: _focusNode,
                canRequestFocus: isEnabled,
                skipTraversal: !isEnabled,
                onFocusChange: _handleFocusChange,
                child: _ButtonStateScope(
                  notifier: _statesController,
                  child: Builder(
                    builder: (context) {
                      final MouseCursor? cursor = widget.mouseCursor?.resolve(
                        Button.of(context)?.states ?? <WidgetState>{},
                      );
                      return MouseRegion(
                        onEnter: isEnabled ? _handlePointerEnter : null,
                        onExit: isEnabled ? _handlePointerExit : null,
                        hitTestBehavior: HitTestBehavior.deferToChild,
                        cursor: cursor ?? MouseCursor.defer,
                        child: child,
                      );
                    },
                  ),
                ),
              ),
            ),
          ),
        );
      }
    }
    

    WidgetState is another good approach (as mentioned above). I've also had luck animating WidgetState like so. Let me know if you need the code for AnimatedBoxDecoration and/or how to implement the surface that consumes it.

    Button(
          onPressed: () {
            onSectionTap(section);
          },
          decoration: WidgetStateDecoration.fromMap({
            WidgetState.pressed: AnimatedBoxDecoration(
              duration: const Duration(milliseconds: 100),
              color: Tail.gray200,
              border: depth < 3 ? border : null,
            ),
            WidgetState.any: AnimatedBoxDecoration(
              duration: const Duration(milliseconds: 100),
              color: Tail.lightTransparent,
              border: depth < 3 ? border : null,
            ),
          }),
    //...

    The only problem with this approach is that there is no good way to use simulations, and I use simulations a lot.

  19. flutter-triage-bot commented on Jul 15, 2026

    @flutter-triage-bot

    This issue is marked P1 but has had no recent status updates.

    The P1 label indicates high-priority issues that are at the top of the work list. This is the highest priority level a bug can have if it isn't affecting a top-tier customer or breaking the build. Bugs marked P1 are generally actively being worked on unless the assignee is dealing with a P0 bug (or another P1 bug). Issues at this level should be resolved in a matter of months and should have monthly updates on GitHub.

    Please consider where this bug really falls in our current priorities, and label it or assign it accordingly. This allows people to have a clearer picture of what work is actually planned. Thanks!

  20. TekExplorer commented on Jul 15, 2026

    @TekExplorer
  21. mateusfccp-cos commented on Aug 5, 2026

    @mateusfccp-cos

    What about checkbox? Will we have a RawCheckbox or similar basic component decoupled from Material's Checkbox?

  22. added
    packageflutter/packages repository. See also p: labels.
    and removed
    frameworkflutter/packages/flutter repository. See also f: labels.
    on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority issues at the top of the work listc: proposalA detailed proposal for a change to Fluttercustomer: crowdAffects or could affect many people, though not necessarily a specific customer.p: material_uimaterial_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.team-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions