desen_ui 0.3.0 copy "desen_ui: ^0.3.0" to clipboard
desen_ui: ^0.3.0 copied to clipboard

Web-grade UI components for Flutter, built on the widgets layer only. No Material or Cupertino dependency.

Desen UI #

Web-grade UI components for Flutter, built on the widgets layer only. No Material, no Cupertino.

Docs and live examples: https://derlioapp.github.io/desen_ui/

Status: 0.3.0, before 1.0: a minor release may still change the API, and every such change is listed in the changelog with how to update (VERSIONING.md). Requires Dart 3.13 and Flutter 3.47 or later.

Foundation, overlay engine, localization (13 languages, plus Portuguese (Portugal) and Traditional Chinese) and 51 components plus form fields: button, badge, count, status dot, avatar, image, card, divider, alert, progress, spinner, skeleton, link, breadcrumb, checkbox, radio, radio card, switch, segmented control, slider, range slider, chip, choice chips, tabs, accordion, stepper, list, sidebar, top navigation, bottom navigation, pane header, empty state, pagination, toolbar, popover, tooltip, menu and context menu, dialog, panel and sheet, toast, select, form field, text field (single and multi-line), search field, autocomplete, multi-select, number field, table, scrollbar, calendar, date and date range picker, time picker, file upload, submenu, text magnifier; Form integration (DsFormField and typed form fields, DsValidators).

Principles #

  • Widgets layer only. lib/ never imports material.dart, cupertino.dart, material_ui or cupertino_ui (enforced by a test).
  • No app wrapper required. DsApp is optional; DsScope works under any root, including MaterialApp. Without either, components fall back to a default theme. Layers (menus, selects, comboboxes, popovers, pickers, dialogs, toasts) need an Overlay, and dialogs and panels a Navigator: any WidgetsApp, MaterialApp or DsApp provides both. A tooltip without an Overlay simply shows its control.
  • One seed, every role. Colors and shadows are generated in OKLCH from a single brand color, with a clash rule that keeps selection distinct from danger and success.
  • End-user settings built in: contrast (soft / standard), corner style (sharp / standard / soft / pill), density (compact / touch), light / dark / system.
  • Phones: density follows the platform unless you pin it: iOS and Android get DsDensity.touch (44px tap areas while the visuals stay small, taller rows, body text 16/22), desktop and desktop browsers DsDensity.compact (24px targets per WCAG 2.5.8, body 14).
  • Keyboard focus shows only for keyboard users (:focus-visible), as one line: fields turn their edge into a 2px focus edge (text fields on every focus, as browsers do for inputs), bordered controls swap their border for a 2px ring, filled controls get a ring with a small gap.

Quick start #

dependencies:
  desen_ui: ^0.3.0

A complete first screen:

import 'package:desen_ui/desen_ui.dart';
import 'package:flutter/widgets.dart';

void main() => runApp(
  DsApp(
    theme: DsThemeData(seed: DsSeed.navy),
    home: const HomePage(),
  ),
);

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _email = TextEditingController();

  @override
  void dispose() {
    _email.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final t = DsTheme.of(context);
    return ColoredBox(
      color: t.colors.canvas,
      child: SafeArea(
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 360),
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                spacing: 16,
                children: [
                  Text('Sign up', style: t.typography.title.copyWith(color: t.colors.text)),
                  DsField(
                    label: const Text('Email'),
                    child: DsTextField(
                      controller: _email,
                      keyboardType: TextInputType.emailAddress,
                    ),
                  ),
                  DsButton(
                    onPressed: () => showDsToast(
                      context: context,
                      title: 'Welcome, ${_email.text}',
                    ),
                    child: const Text('Continue'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Inside an existing app:

DsScope(
  theme: DsThemeData(seed: DsSeed.color(const Color(0xFF2D4D8B))),
  child: child,
)

Buttons:

DsButton(onPressed: save, child: const Text('Save'))
DsButton(variant: .secondary, size: .sm, leading: const DsIcon(DsIcons.plus), onPressed: add, child: const Text('New task'))
DsButton.icon(icon: const DsIcon(DsIcons.ellipsis), semanticLabel: 'More', onPressed: openMenu)

// Customize one instance, a subtree, or build a new look on the same behavior:
DsButton(style: DsButtonStyle(borderColor: brand), …)
DsButtonTheme(data: const DsButtonThemeData(size: .sm), child: …)
DsPressable(onPressed: …, builder: (context, states, _) => …)

Customizing generated tokens (survives dark mode, contrast, corner and density switches):

DsThemeData(
  seed: DsSeed.color(brand),
  adjustColors: (k, brightness) => k.copyWith(link: …),
  adjustRadii: (r, style) => r.copyWith(card: r.card + 4),
)

Component defaults for the whole app or one section (every component, one mechanism):

DsComponentThemes(
  themes: const [
    DsButtonThemeData(size: .sm),
    DsChipThemeData(style: DsChipStyle(height: 28)),
  ],
  child: app,
)

Floating layers are opaque. To make them frosted glass, give them a translucent fill and a backdrop filter. Keep the fill dense enough that text on it still reads over any content; the contrast budget assumes opaque layers.

import 'dart:ui' show ImageFilter;

DsApp(
  builder: (context, child) {
    final colors = DsTheme.colorsOf(context);
    final glass = ImageFilter.blur(sigmaX: 24, sigmaY: 24);
    final fill = colors.overlay.withValues(alpha: .8);
    return DsComponentThemes(
      themes: [
        DsMenuThemeData(style: DsMenuStyle(background: fill, backdropFilter: glass)),
        DsPopoverThemeData(style: DsPopoverStyle(background: fill, backdropFilter: glass)),
        DsBottomNavThemeData(style: DsBottomNavStyle(background: fill, backdropFilter: glass)),
        // also DsDialog, DsPanel, DsToast, DsToolbar, DsTooltip, DsTextSelectionToolbar
      ],
      child: child!,
    );
  },
  home: const HomePage(),
)

DsSurface draws the same kind of box for your own floating layers.

Reading tokens:

final colors = DsTheme.colorsOf(context); // rebuilds only when colors change
final t = DsTheme.of(context);            // everything

Platforms, size and languages #

  • Platforms: iOS, Android, web, macOS, Windows and Linux; widgets only, no platform plugins. Density, the text font and haptics follow the platform.
  • Size: the bundled fonts add about 600 KB to every build (iOS and macOS included, even though they set text in the system font by default). Icons an app does not use are tree-shaken away.
  • Languages: strings for 13 languages, plus Portuguese (Portugal) and Traditional Chinese, with right-to-left layout for Arabic. DsApp resolves to its locale, or English; to follow the device, pass the languages your app is translated into (or DsLocalizations.supportedLocales) as supportedLocales. Add or override strings with DsLocalizationScope. See Localization.

Fonts and icons #

Desen bundles Schibsted Grotesk for text and Geist Mono for code, so they need no setup (SIL Open Font License 1.1, see License). iOS and macOS apps set text in the system font (San Francisco) by default; pass a family to DsTypography to use another face there too. DsIcons is a fork of the free Hugeicons set: 5388 stroke icons from Hugeicons 4.3.5 (MIT), a few of them put together by Desen from Hugeicons parts where the free set has a gap (a plain clipboard, star-plus, eject), kept in this repository and updated only after review, drawn from SVG paths with no font asset; an icon an app does not use adds nothing to its size. Desen names the icons itself, after what they show and with Lucide's naming rules, in camel case (circle-check is DsIcons.circleCheck); where Lucide calls the same shape something else, that name works too (DsIcons.home is house). Components take icons as widgets, so any other icon set works too.

Testing your app #

Keyboard focus visibility is global state, like the browser's :focus-visible. Desen resets it when the test binding resets between tests; to pin the starting value (for example, to test focus rings as a desktop user sees them first), call:

setUp(() => DsFocusVisibility.debugReset(keyboard: true));

Known limitations #

  • Screen readers aren't told which menu item opens a submenu (needs a role Flutter doesn't expose yet).
  • Screen readers announce the select as a button with its expanded state, and the number field as an adjustable text field: Flutter doesn't support the combobox and spin button roles yet.
  • Plain FormState.validate() announces errors through Flutter's own unnamed announcement. formKey.currentState!.validateAndFocus() (from DsFormValidation) focuses the first invalid field and announces it once, with its name.
  • VoiceOver users who turned hints off don't hear a field's description or error, which are attached as hints.
  • Visual references (golden images) are rendered on macOS and checked by hand on the web and macOS; iOS and Android have not yet been checked on physical devices.

Layout #

Path Contents
lib/src/foundation/ OKLCH conversion, contrast helpers, SVG path parser
lib/src/icons/ DsIcon, DsIcons
lib/src/painting/ DsShadow (with inset), DsBoxDecoration, DsSurface
lib/src/theme/ Palette engine, color roles, shadows, radii, sizes, motion, typography, DsTheme, DsScope
lib/src/app/ DsApp, DsPageRoute, DsScrollBehavior
lib/src/behavior/ Headless behavior (DsPressable, DsMinTapTarget, DsFocusVisibility)
lib/src/overlay/ Anchored layers (DsAnchoredOverlay, placement), modal route
lib/src/l10n/ DsLocalizations (13 languages and 2 regional variants, generated)
lib/src/components/ Styled components
fonts/ Schibsted Grotesk and Geist Mono, with their licenses (SIL OFL 1.1)
VERSIONING.md What counts as a breaking change, pre-1.0 rules, deprecation
example/ The docs site (example/lib/site/, English, built with Desen): every component with live examples and code, foundations, guides and app examples

Development #

Main stays green: flutter analyze, the format check and the full flutter test run before every commit (about 40 seconds). A new test must fail without its fix. Goldens are re-baselined only after reviewing the diff images, in a commit of their own.

flutter test                                  # everything
flutter test --tags golden                    # visual regression only
flutter test --update-goldens --tags golden   # after an intended visual change; review the PNGs
tool/native_snapshot.sh /tmp dark             # native macOS (Impeller) render of the example
python3 tool/gen_colors.py lib/src/theme/colors.dart  # regenerate DsColors
python3 tool/gen_icons.py                              # regenerate DsIcons from tool/icons/
python3 tool/import_icons.py --hugeicons <version>      # take Hugeicons changes into tool/icons/ for review
python3 tool/gen_styles.py                            # regenerate component styles/themes
python3 tool/gen_l10n.py                              # regenerate DsLocalizations

License #

The code is under the MIT License (LICENSE). The font files in fonts/ are not: they are under the SIL Open Font License 1.1, which allows bundling and embedding them in any app, free or commercial. Its text is in fonts/ and in NOTICES, which Flutter adds to your app's license page (showLicensePage, LicenseRegistry) on its own.

  • Schibsted Grotesk: Copyright 2023 The Schibsted-Grotesk Project Authors; license text in fonts/OFL-SchibstedGrotesk.txt.
  • Geist Mono: Copyright 2024 The Geist Project Authors; license text in fonts/OFL-GeistMono.txt.

The icon shapes are from the free Hugeicons set (MIT License, included in NOTICES).

0
likes
160
points
339
downloads

Documentation

Documentation
API reference

Publisher

verified publisherderlio.app

Weekly Downloads

Web-grade UI components for Flutter, built on the widgets layer only. No Material or Cupertino dependency.

Repository (GitHub)
View/report issues

Topics

#ui #design-system #widget #accessibility #components

License

MIT (license)

Dependencies

flutter

More

Packages that depend on desen_ui