Skip to content

Design-System Components

View raw .md
On this page

Introduction#

Magic Starter ships a complete atomic design-system component library under lib/src/ui/components/. Every component is driven by a WindRecipe that resolves colors through the MagicStarterTokens semantic alias layer, so a single brand configuration flows into all built-in screens and every component your host app composes directly.

All 46 components are exported from package:magic_starter/magic_starter.dart.

Semantic Token Layer#

MagicStarterTokens#

MagicStarterTokens is a single const class with one public member: defaultAliases. It is a Map<String, String> of 17 semantic roles to light+dark Wind className pairs following the bg-/text-/border-color- prefix convention:

dart
final aliases = MagicStarterTokens.defaultAliases;
// {
//   'surface':                  'bg-white dark:bg-gray-950',
//   'surface-container':        'bg-gray-50 dark:bg-gray-900',
//   'surface-container-high':   'bg-gray-100 dark:bg-gray-800',
//   'fg':                       'text-gray-900 dark:text-gray-50',
//   'fg-muted':                 'text-gray-500 dark:text-gray-400',
//   'fg-disabled':              'text-gray-300 dark:text-gray-600',
//   'primary':                  'bg-primary-600 dark:bg-primary-500',
//   'on-primary':               'text-white dark:text-white',
//   'primary-container':        'bg-primary-50 dark:bg-primary-950',
//   'accent':                   'bg-accent-600 dark:bg-accent-500',
//   'border':                   'border-gray-200 dark:border-gray-800',
//   'border-subtle':            'border-gray-100 dark:border-gray-900',
//   'destructive':              'bg-red-600 dark:bg-red-500',
//   'on-destructive':           'text-white dark:text-white',
//   'destructive-container':    'bg-red-50 dark:bg-red-950',
//   'success':                  'bg-green-600 dark:bg-green-500',
//   'warning':                  'bg-amber-500 dark:bg-amber-400',
// }

Wiring into WindThemeData#

Pass MagicStarterTokens.defaultAliases as the aliases argument when constructing your WindThemeData:

dart
WindApp(
  theme: WindThemeData(
    aliases: MagicStarterTokens.defaultAliases,
  ),
  child: const MyApp(),
)

Components that reference a semantic role (e.g. bg-surface, text-fg, bg-primary) will resolve through this map at render time. Without the aliases wired in, the semantic class names are silently skipped by Wind, so your brand color and surface tokens will not apply.

Token Reference#

Role Semantic meaning Maps to (default)
surface Page/card background bg-white dark:bg-gray-950
surface-container Subtle recessed area bg-gray-50 dark:bg-gray-900
surface-container-high Elevated container bg-gray-100 dark:bg-gray-800
fg Primary text text-gray-900 dark:text-gray-50
fg-muted Secondary/supporting text text-gray-500 dark:text-gray-400
fg-disabled Disabled state text text-gray-300 dark:text-gray-600
primary Brand primary fill bg-primary-600 dark:bg-primary-500
on-primary Text on primary fill text-white dark:text-white
primary-container Tinted primary area bg-primary-50 dark:bg-primary-950
accent Secondary brand fill bg-accent-600 dark:bg-accent-500
border Standard border border-gray-200 dark:border-gray-800
border-subtle Faint separator border-gray-100 dark:border-gray-900
destructive Danger / delete fill bg-red-600 dark:bg-red-500
on-destructive Text on danger fill text-white dark:text-white
destructive-container Tinted danger area bg-red-50 dark:bg-red-950
success Positive / confirmed bg-green-600 dark:bg-green-500
warning Caution / review bg-amber-500 dark:bg-amber-400
bg-ai-soft Soft container for AI-authored content (replies, summaries) bg-violet-100 dark:bg-violet-900
text-ai-soft-foreground Text or icon on top of bg-ai-soft text-violet-700 dark:text-violet-200
border-ai Accent edge of AI-authored content border-violet-500 dark:border-violet-400
bg-warning-soft Soft amber container for a quiet status pill bg-amber-100 dark:bg-amber-900
text-warning-soft-foreground Text or icon on top of bg-warning-soft text-amber-800 dark:text-amber-200
bg-success-soft Soft green container for a quiet status pill bg-green-100 dark:bg-green-900
text-success-soft-foreground Text or icon on top of bg-success-soft text-green-800 dark:text-green-200

These keys are written as the full class they replace (bg-ai-soft, border-ai), not as a bare role. The support screens use the AI pair and border-ai for the assistant's avatar, its bubble (a 2px left edge) and its chip, and the soft warning and success pairs for the "Team answering" and "Resolved" pills. A host theme that defines the same keys supplies its own map and wins.

To override individual roles, spread defaultAliases and replace specific keys:

dart
WindThemeData(
  aliases: {
    ...MagicStarterTokens.defaultAliases,
    'primary': 'bg-indigo-600 dark:bg-indigo-500',
    'on-primary': 'text-white dark:text-white',
  },
)

Atomic Folder Shape#

Every component lives in a 4-file atomic folder:

text
lib/src/ui/components/<name>/
  <name>.dart          # The widget class (StatelessWidget or StatefulWidget)
  <name>.recipe.dart   # WindRecipe function: resolves className from theme tokens
  <name>.preview.dart  # Standalone preview widget for interactive inspection
  index.dart           # Barrel: re-exports the public API for this component

The .recipe.dart file contains a top-level function (e.g. buildButtonRecipe, buildInputRecipe, cardRecipe) that accepts variant/state arguments and returns a Wind className string. The widget delegates its className derivation to the recipe so the visual contract is inspectable and testable in isolation.

Component Families#

Form Controls#

Component Enums / Helpers Notes
MSButton ButtonIntent (primary/secondary/ghost/destructive), ButtonSize (sm/md/lg), buildButtonRecipe Use ButtonIntent.destructive for delete / danger actions; fullWidth: true fills the parent width (wraps in SizedBox, defaults false). Shape, the three sizes and the secondary border come from MagicStarterButtonTheme (MagicStarter.useButtonTheme). Below lg, every size keeps its painted size but takes a 44x44 hit area
MSInput InputState (idle/focused/error/disabled), buildInputRecipe Pairs with MSFormField for label + error display; fullWidth: true fills the parent width (defaults false). A single-line field is 44px tall below lg and 40px from lg up (py-3 lg:py-2.5 over a leading-5 line); the padding and type of the text, placeholder and value come from MagicStarterFieldTheme.inputSizeClassName (MagicStarter.useFieldTheme)
MSTextarea TextareaState, textareaRecipe Multi-line input, same state model as MSInput; fullWidth: true fills the parent width (defaults false). On iOS a focused field carries a Done toolbar above the keyboard, since a multiline field's Return key inserts a newline and would otherwise leave the keyboard with no way out. One consequence for consumer TESTS: while that toolbar is up it schedules a frame callback every frame, so pumpAndSettle under debugDefaultTargetPlatformOverride = TargetPlatform.iOS never settles; pump explicitly instead
MSCheckbox Boolean toggle; Wind-only, no Material dependency
MSSwitch Toggle; replaces Flutter's MSSwitch in Wind layouts. An optional label renders beside the track and is part of the tap target; below lg the hit area is at least 44 px tall. Shows a focus ring on keyboard focus
MSRadio Single-select option
MSSelect selectRecipe Dropdown single-select backed by an item list; the trigger is the same height as MSInput (44px below lg, 40px from lg up), and its padding, minimum height and type come from MagicStarterFieldTheme.selectSizeClassName
MSCombobox comboboxRecipe Searchable single-select with filter input; the trigger is the same height as MSInput (44px below lg, 40px from lg up) and shares MagicStarterFieldTheme.selectSizeClassName with MSSelect. The search field's placeholder defaults to trans('common.search_placeholder')
MSSwitchRow switchRowRecipe An MSSwitch with its label beside it; the label doubles as the switch's accessibility name
MSKeyValueEditor MSKeyValueRow, keyValueEditorRecipe Controlled editor for an ordered list of key/value pairs (e.g. HTTP headers); every mutation emits a fresh list
MSStringValueList StringValueListTone, stringValueListRecipe Controlled editor for a short list of distinct strings rendered as chips; commits on Enter/IME-done, trims and de-duplicates

Display and Feedback#

Component Enums / Helpers Notes
MSBadge BadgeTone (neutral/primary/success/warning/destructive) Inline label chip
MSTypography TypographyVariant (h1-h6/body/caption/overline) Semantic text wrapper
MSSkeleton SkeletonShape (line/rect/circle) Placeholder loading block
MSToast ToastVariant (info/success/warning/error) Transient feedback banner (the message is text-sm font-medium; messageClassName is appended after it, so text-[15px] leading-5 font-normal sets another body type, with or without an action); presents nothing itself, so the host shows it (through its own OverlayEntry) and decides how long it stays. Magic.toast(String) cannot carry an action and its overlay ignores pointers, so an action needs the host's own overlay. actionLabel and onAction (both or neither) add a trailing text action such as Undo: a cell at least 44 px tall and wide at every width (it grows with the label at a large text size), named by actionLabel for assistive technology, with a ring inside the toast on keyboard focus. A long message wraps beside it and the action keeps its size; a one line toast is as tall with an action as without
MSTooltip Hover/long-press hint bubble
MSEmptyState Illustrated empty-list placeholder
MSErrorState Full-screen or inline error with retry
MSDataTable MSDataColumn, dataTable*ClassName Header plus rows, with the columns passed in. Two modes: the default renders rows eagerly (right for a short, complete list); MSDataTable.paginated hands the body to magic's MagicPaginatedListView inside a bounded box, so a long collection costs the viewport rather than the result and reaching the tail asks the paginator for its next page. The header stays outside the scrolling body. bodyHeight is what bounds it, since a ListView needs a bound and shrinkWrap: true would build every row anyway. Column labels and loadingLabel are ALREADY TRANSLATED strings, not keys: half the callers render a label that is not a key at all (a currency code, a region name)

Selection and Navigation#

Component Enums / Helpers Notes
MSSegmentedControl SegmentedControlSize, segmentedControlRecipe Inline tab switcher; below lg each segment is at least 44 px tall to tap, the painted pill unchanged. The pill's padding and type per size and the root's padding come from MagicStarterFieldTheme (segmentedSmClassName, segmentedMdClassName, segmentedPaddingClassName); the hit cell is not themed. In a horizontal scroller (the overflow-x-auto root override, or your own scroll view that holds nothing but the control) it scrolls the selected segment into view on mount and when selectedIndex changes; a page view, a vertical list or a strip with padding or other content around the control is left alone, and the page's vertical scroll never moves
MSTabs tabsRecipe Full tab bar with content panels
MSAccordion MSAccordionItem, accordionRecipe Collapsible section list
MSNavbar Horizontal top navigation bar
MSDropdownMenu MSDropdownMenuItem Contextual action menu

Overlay#

Component Notes
MSDialog Modal dialog shell; reads MagicStarterModalTheme tokens. scrollable: false hands a body that scrolls itself the bounded height, as on MSBottomSheet. With the modal theme's presentation: MagicStarterModalPresentation.sheet, MSDialog.show opens the same content as a bottom sheet, and the widget draws the sheet panel inside a bottom sheet route
MSBottomSheet Slide-up sheet; reads MagicStarterModalTheme tokens. MSBottomSheet.show(..., dialogBreakpoint: 'lg') presents an MSDialog from that Wind breakpoint up, with the same title, description, body, footer and scrollable, so one call serves a phone and a desktop; the width is read when the call is made. Null (the default) always presents a sheet, and so does the sheet presentation, which ignores the breakpoint; the adaptive presentation ignores it too and swaps at MagicStarterModalTheme.dialogBreakpoint for every call. Built in a dialog route (an overlay route under adaptive), the widget draws the dialog shell. canDismiss (a ValueListenable<bool>, read at the gesture) holds a sheet with unsaved input open: while false a drag down is off (the handle stays drawn), and a scrim tap or its semantics dismiss, Back and Escape leave it open and call onDismissBlocked(sheetContext), where the app asks the person and, on a confirmed discard, closes it with Navigator.of(sheetContext).pop() (a pop skips the guard; the opener's context would pop the wrong navigator, since the sheet sits on the root one); the dialog from dialogBreakpoint is held the same way

Composition and App Chrome#

Component Notes
MSFormField Label + input + hint + error layout wrapper
MSCard Surface/inset/elevated variants
MSPageHeader Full-width responsive header (title, subtitle, leading, actions). Stacked below sm, a row above it; inlineActions (or MagicStarterPageHeaderTheme.inlineActions) makes it a row at every width AND lets a long title shrink instead of overflowing. size picks MSPageHeaderSize.regular, .large (the theme's largeTitleClassName, display size on a phone and one step smaller from lg) or .compact (a 44 pt bar below the top safe area: leading control, centred one-line title, actions, over a translucent fill and a backdrop blur that turn opaque under high contrast). Null follows MagicStarterPageHeaderTheme.largeTitle. className is appended to the container after the theme's container class, for a page under a bar of its own (pt-0 or lg:pt-0 takes the header's top padding off); it styles the regular and the large header, and the compact bar ignores it. Under MagicStarter.useHostPageChrome, where the predicate answers true, it draws only its actions and reports its title, subtitle and back control to MagicStarter.pageChrome (host-owned page chrome)
MSSocialDivider "Or continue with" separator for auth forms
MSUserProfileDropdown Avatar menu with profile links, theme toggle and logout
MSTeamSelector Current-team switcher; requires a registered team resolver
MSPageContainer Shared page geometry: width cap, edge margins, vertical rhythm, horizontal safe area. Reads MagicStarter.manager.pageContainerClassName
MSPageScaffold Full page treatment: page surface + own scroll + MSPageContainer + MSPageHeader + gap-6 sections column. footer docks a widget below the scroll area, outside it, in the same width and edge margins; it clears the bottom safe area and rides above the on-screen keyboard (a chat composer is the use). A docked layout needs a bounded height, as the app shell provides. scrollController hands the scroll area to the page, to keep a thread on its newest message. containerClassName is appended to the page container after the host's geometry, for a page under a bar of its own (pt-4 replaces the host's top padding). headerClassName is handed to the regular and the large header the same way (lg:pt-0 takes the header's own top padding off from lg up); the compact bar is built without it. largeTitle: true (or MagicStarterPageHeaderTheme.largeTitle, which also reaches the starter's own pages) renders a large header that collapses into the compact bar pinned over the page once it has scrolled under it, fading in 200 ms (a swap under Reduce Motion). Under host-owned page chrome it builds neither the large title nor the bar
MSMasterDetail A list and its selected item side by side from breakpoint (lg by default): master in a fixed w-96 column, detail (or placeholder while it is null) in the rest. Below the breakpoint it renders detail ?? master, which is push navigation when the selection lives in the route. Owns no page geometry or scroll, so it goes inside MSPageScaffold; MSMasterDetail.isSplit(context) gives a page the same answer for its header
MSFormActions Cancel/submit footer row for a form; cancelLabel/onCancel are optional (omitting both renders no cancel button)
MSHeaderAction Page-header action that collapses to an icon-only glyph below lg and a labelled MSButton at lg and up; label becomes the icon form's accessibility label

Import Collisions#

Every component carries an MS prefix (MSButton, MSDialog, MSSwitch, ...), so none of the barrel's component exports collide with package:flutter/material.dart. You can import both without a hide clause:

dart
import 'package:flutter/material.dart';
import 'package:magic_starter/magic_starter.dart';

// No ambiguity: MSSwitch is the component, Switch is Material's.

Earlier releases exported unprefixed names (Switch, Dialog, ...) and required a hide clause. See the migration table in the package README to update from the pre-MS names.

Or hide the specific colliding names:

dart
import 'package:flutter/material.dart' hide MSSwitch, MSDialog, MSCheckbox;
import 'package:magic_starter/magic_starter.dart';

Widget tests that need both Material and magic_starter types must do the same.

design.md.stub#

assets/stubs/design.md.stub is a DESIGN.md template that covers all 17 semantic roles, typography on the 4px logical scale, rounded/spacing scales, and key component entries with {{ placeholder }} tokens.

Consumers copy it into their project root:

bash
cp $(flutter pub cache list magic_starter)/assets/stubs/design.md.stub DESIGN.md

Then fill in brand hex values and fonts, and run the CLI commands:

bash
# Validate your DESIGN.md against the schema
dart run <app>:artisan design:lint

# Generate the Wind theme from your DESIGN.md
dart run <app>:artisan design:sync

design:sync regenerates the WindThemeData(aliases: ...) map, replacing MagicStarterTokens.defaultAliases with your brand-specific values so every component reflects your brand automatically.

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.