brutalist_ui

CI pub package License: MIT

A neobrutalist design library for Flutter, built entirely on package:flutter/widgets.dart.

No Material. No Cupertino. Thick hard borders, flat saturated fills, and solid offset shadows with no blur. Pressing a control translates it by its shadow offset and collapses the shadow, so the element presses into the page.

Buttons, sizes, text input and selection controls

Why this exists

Flutter is moving the Material and Cupertino design libraries out of the SDK and onto pub.dev as material_ui and cupertino_ui (both published by the flutter.dev verified publisher, first release 2026-07-21). The core framework keeps the raw primitives; design systems become ordinary packages.

This package is built for that world. Its only Flutter imports are widgets.dart, foundation.dart, services.dart, and gestures.dart, and it composes the SDK's headless primitives — RawRadio, ToggleableStateMixin, FocusableActionDetector, WidgetStatesController, EditableText, showGeneralDialog — the same way cupertino_ui does. Nothing here breaks when the in-SDK material.dart and cupertino.dart are removed.

Install

Not yet published. Depend on it by path or git:

dependencies:
  brutalist_ui:
    path: ../brutalist_ui

Use

The barrel re-exports package:flutter/widgets.dart, so one import is enough:

import 'package:brutalist_ui/brutalist_ui.dart';

void main() => runApp(
  NeoApp(
    theme: const NeoThemeData(),
    darkTheme: const NeoThemeData.dark(),
    home: NeoScaffold(
      appBar: const NeoAppBar(title: Text('NEOBRUTALIST')),
      body: Center(
        child: NeoButton(
          onPressed: () {},
          child: const Text('PRESS ME'),
        ),
      ),
    ),
  ),
);

Theming

NeoThemeData holds five tokens: colors, typography, borderWidth, borderRadius, and shadowOffset. brightness is derived from the color scheme rather than stored separately, so a theme can't disagree with its own colors.

NeoApp(
  theme: NeoThemeData(
    colors: const NeoColorScheme.light().copyWith(primary: Color(0xFFB8FF4F)),
    borderWidth: 4,
    shadowOffset: const Offset(6, 6),
  ),
  home: ...,
)

NeoTheme.of(context) returns the nearest theme, falling back to const NeoThemeData() so components still work without an enclosing NeoApp.

Components

App & layout NeoApp, NeoScaffold, NeoAppBar, NeoBottomNavBar, NeoPageRoute
Actions NeoButton, NeoIconButton, NeoPressable
Selection NeoCheckbox, NeoSwitch, NeoRadio, NeoChip, NeoDropdown
Input NeoTextField, NeoTextFormField, NeoSlider
Feedback NeoDialog + showNeoDialog, NeoBottomSheet + showNeoSheet, NeoTooltip, NeoProgressBar, NeoActivityIndicator
Surfaces NeoBox, NeoCard, NeoListTile, NeoDivider, NeoBadge, NeoScrollbar
Text selection NeoTextSelectionControls, NeoTextSelectionToolbar, NeoMagnifier
Tokens NeoThemeData, NeoColorScheme, NeoTypography, NeoVariant, NeoIcon

NeoVariant (primary, secondary, accent, success, warning, danger, neutral) is the shared color vocabulary across buttons, chips, badges, and toggles.

NeoBox (static) and NeoPressable (interactive) are the two primitives every other component is built from — reach for them to build your own.

NeoIcon draws its glyphs with CustomPaint, so the package needs no icon font and does not require uses-material-design: true.

Accessibility

Every interactive component reports the right semantics: buttons as buttons, NeoCheckbox with checked state, NeoSwitch as toggled, NeoRadio through RawRadio's own semantics, and NeoSlider with a value plus increase/decrease actions. Controls are keyboard-focusable and activate on Enter/Space via FocusableActionDetector. Focus is shown by recoloring the hard shadow, which avoids the layout shift an added outline would cause.

Example

cd example && flutter run

The gallery covers every component and toggles light/dark at runtime.

Text selection

NeoTextField supports the full selection stack: draggable square handles, a hard-edged copy/cut/paste/select-all toolbar, and a magnifier — all built on TextSelectionControls, TextSelectionGestureDetectorBuilder, and RawMagnifier.

Context-menu labels default to English because the package takes no dependency on flutter_localizations or intl. To localize, pass labelFor to NeoTextSelectionToolbar.buttonsFor from your own contextMenuBuilder:

NeoTextField(
  contextMenuBuilder: (BuildContext context, EditableTextState state) {
    final TextSelectionToolbarAnchors anchors = state.contextMenuAnchors;
    return NeoTextSelectionToolbar(
      anchorAbove: anchors.primaryAnchor,
      anchorBelow: anchors.secondaryAnchor ?? anchors.primaryAnchor,
      children: NeoTextSelectionToolbar.buttonsFor(
        context,
        state.contextMenuButtonItems,
        labelFor: (ContextMenuButtonItem item) => myLocalizedLabel(item),
      ),
    );
  },
)

Known limitations

  • Context-menu and selection-toolbar labels are English by default (see above).
  • NeoRadio requires a RadioGroup<T> ancestor (the current Flutter radio API).
  • NeoScrollbar thumb has no border — RawScrollbar paints a plain rect, so only the track carries the hard border.
  • No date/time picker, tabs, snackbar, or accordion yet — see the roadmap.

Roadmap

Next, in rough order: NeoTabs, NeoSnackbar, NeoSegmentedControl, NeoAccordion, NeoStepper, NeoAvatar, NeoSearchField, pull-to-refresh, grouped list sections, a skeleton loader, and date/time pickers (which will introduce the flutter_localizations + intl dependency).

Contributing

Pull requests are welcome. Start with CONTRIBUTING.md — it explains the one hard rule (no Material, no Cupertino in lib/, enforced by CI), how components are structured, and what to run before opening a pull request.

By participating you agree to the Code of Conduct. To report a security issue, see SECURITY.md — please do not open a public issue for those.

License

MIT — see LICENSE.

Libraries

brutalist_ui
A neobrutalist design library for Flutter.