remix-ui

star 17

Guide for building Flutter UIs with the Remix component library (Mix + Naked UI + Fortal theme). Use this skill whenever the user wants to create Flutter UI with Remix components, style Remix widgets, set up Fortal theming, customize component appearance, handle interaction states, or asks about any Remix widget (RemixButton, RemixCheckbox, RemixSelect, RemixTabs, etc.). Also trigger when the user mentions Remix styling, Fortal tokens, or wants help with Flutter component design using this design system — even if they just say "make a button" or "style this form" in a Remix project.

btwld By btwld schedule Updated 3/23/2026

name: remix-ui description: Guide for building Flutter UIs with the Remix component library (Mix + Naked UI + Fortal theme). Use this skill whenever the user wants to create Flutter UI with Remix components, style Remix widgets, set up Fortal theming, customize component appearance, handle interaction states, or asks about any Remix widget (RemixButton, RemixCheckbox, RemixSelect, RemixTabs, etc.). Also trigger when the user mentions Remix styling, Fortal tokens, or wants help with Flutter component design using this design system — even if they just say "make a button" or "style this form" in a Remix project.

Remix UI — Building Flutter Interfaces

Remix is a Flutter design-system library. It combines Naked UI (headless accessible behavior) with Mix (styling engine) to deliver fully styled, interaction-aware components. The built-in Fortal theme provides Radix-inspired design tokens out of the box.

Quick Start

import 'package:remix/remix.dart';

Wrap your app (or a subtree) in FortalScope to enable the Fortal design tokens:

FortalScope(
  accent: FortalAccentColor.indigo,  // 31 color options
  gray: FortalGrayColor.slate,       // 6 neutral scales
  brightness: Brightness.light,      // or .dark
  child: MaterialApp(home: MyScreen()),
)

Every Remix component works without FortalScope, but Fortal preset styles (like FortalButtonStyle.solid()) require it to resolve tokens.


Component Catalog

Remix ships 20 components. Each accepts a style parameter for full visual control.

Category Components
Actions RemixButton, RemixIconButton
Forms RemixCheckbox, RemixRadio + RemixRadioGroup, RemixSwitch, RemixSlider, RemixTextField, RemixSelect
Data display RemixAvatar, RemixBadge, RemixCard, RemixCallout, RemixProgress, RemixSpinner, RemixDivider
Overlays RemixDialog (+ showRemixDialog), RemixTooltip, RemixMenu
Navigation RemixTabs + RemixTabBar + RemixTab + RemixTabView, RemixAccordion + RemixAccordionGroup

See references/components.md for the full API of every component.


Using Components

Buttons

RemixButton(
  label: 'Submit',
  onPressed: () => handleSubmit(),
  style: FortalButtonStyle.solid(size: FortalButtonSize.size2),
)

RemixButton(
  label: 'Delete',
  leadingIcon: Icons.delete,
  loading: isDeleting,
  enabled: canDelete,
  onPressed: () => handleDelete(),
  style: FortalButtonStyle.outline(),
)

RemixIconButton(
  icon: Icons.settings,
  onPressed: () => openSettings(),
  style: FortalIconButtonStyle.ghost(),
)

Form Controls

// Checkbox
RemixCheckbox(
  selected: isChecked,
  onChanged: (val) => setState(() => isChecked = val),
  style: FortalCheckboxStyles.surface(),
)

// Radio group
RemixRadioGroup<String>(
  groupValue: selectedOption,
  onChanged: (val) => setState(() => selectedOption = val),
  child: Column(children: [
    RemixRadio(value: 'a'),
    RemixRadio(value: 'b'),
    RemixRadio(value: 'c'),
  ]),
)

// Switch
RemixSwitch(
  selected: isDarkMode,
  onChanged: (val) => toggleDarkMode(val),
  style: FortalSwitchStyles.soft(),
)

// Slider
RemixSlider(
  value: volume,
  onChanged: (val) => setState(() => volume = val),
  min: 0,
  max: 100,
)

// TextField
RemixTextField(
  controller: emailController,
  label: 'Email',
  hintText: 'you@example.com',
  helperText: 'We will never share your email',
  error: hasError,
  style: FortalTextFieldStyles.surface(),
)

Select & Menu

// Select (dropdown)
RemixSelect<String>(
  trigger: RemixSelectTrigger(placeholder: 'Choose a fruit'),
  items: [
    RemixSelectItem(value: 'apple', label: 'Apple'),
    RemixSelectItem(value: 'banana', label: 'Banana'),
    RemixSelectItem(value: 'cherry', label: 'Cherry'),
  ],
  selectedValue: selectedFruit,
  onChanged: (val) => setState(() => selectedFruit = val),
  style: FortalSelectStyles.surface(),
)

// Menu (action menu)
RemixMenu<String>(
  trigger: RemixMenuTrigger(label: 'Actions', icon: Icons.more_vert),
  items: [
    RemixMenuItem(value: 'edit', label: 'Edit', leadingIcon: Icons.edit),
    RemixMenuItem(value: 'copy', label: 'Copy', leadingIcon: Icons.copy),
    RemixMenuDivider(),
    RemixMenuItem(value: 'delete', label: 'Delete', leadingIcon: Icons.delete),
  ],
  onSelected: (action) => handleAction(action),
)

Tabs

RemixTabs(
  selectedTabId: currentTab,
  onChanged: (id) => setState(() => currentTab = id),
  child: Column(children: [
    RemixTabBar(
      child: Row(children: [
        RemixTab(tabId: 'overview', label: 'Overview'),
        RemixTab(tabId: 'details', label: 'Details', icon: Icons.info),
        RemixTab(tabId: 'settings', label: 'Settings'),
      ]),
    ),
    Expanded(child: Column(children: [
      RemixTabView(tabId: 'overview', child: OverviewPanel()),
      RemixTabView(tabId: 'details', child: DetailsPanel()),
      RemixTabView(tabId: 'settings', child: SettingsPanel()),
    ])),
  ]),
)

Accordion

RemixAccordionGroup<String>(
  controller: accordionController,
  child: Column(children: [
    RemixAccordion(value: 'faq1', title: 'What is Remix?', child: Text('...')),
    RemixAccordion(value: 'faq2', title: 'How does theming work?', child: Text('...')),
  ]),
)

Data Display

// Avatar
RemixAvatar(
  label: 'JD',
  backgroundImage: NetworkImage('https://...'),
  style: FortalAvatarStyles.soft(size: FortalAvatarSize.size3),
)

// Badge
RemixBadge(
  label: 'New',
  style: FortalBadgeStyles.solid(),
)

// Card
RemixCard(
  style: FortalCardStyles.surface(size: FortalCardSize.size2),
  child: Column(children: [
    Text('Card Title'),
    Text('Card content goes here'),
  ]),
)

// Callout
RemixCallout(
  icon: Icons.info,
  text: 'This is an informational callout.',
  style: FortalCalloutStyles.surface(),
)

// Progress
RemixProgress(value: 0.65, style: FortalProgressStyles.surface())

// Spinner
RemixSpinner(style: FortalSpinnerStyles.create(size: FortalSpinnerSize.size2))

// Divider
RemixDivider(style: FortalDividerStyles.create(size: FortalDividerSize.size1))

Overlays

// Dialog
showRemixDialog(
  context: context,
  builder: (_) => RemixDialog(
    title: 'Confirm',
    description: 'Are you sure you want to proceed?',
    actions: [
      RemixButton(label: 'Cancel', onPressed: () => Navigator.pop(context),
        style: FortalButtonStyle.outline()),
      RemixButton(label: 'Confirm', onPressed: () { confirm(); Navigator.pop(context); },
        style: FortalButtonStyle.solid()),
    ],
  ),
)

// Tooltip
RemixTooltip(
  tooltipChild: Text('This button saves your work'),
  child: RemixIconButton(icon: Icons.save, onPressed: save),
)

Styling Components

Every component takes a style parameter. There are two approaches:

1. Fortal Presets (Recommended for Consistency)

Fortal styles give you pre-built variants and sizes that follow the design system:

// Variant + size
FortalButtonStyle.solid(size: FortalButtonSize.size3)
FortalButtonStyle.outline()
FortalButtonStyle.ghost()
FortalButtonStyle.soft()
FortalButtonStyle.surface()

// Or via create():
FortalButtonStyle.create(
  variant: FortalButtonVariant.solid,
  size: FortalButtonSize.size2,
)

See references/fortal-reference.md for all variants and sizes per component.

2. Custom Styles (Full Control via Fluent API)

Build styles from scratch using the chainable fluent API:

RemixButtonStyle()
    .color(Colors.blue)
    .borderRounded(12)
    .paddingX(24)
    .paddingY(12)
    .labelColor(Colors.white)
    .labelFontSize(16)
    .labelFontWeight(FontWeight.w600)
    .iconColor(Colors.white)
    .iconSize(20)
    .spacing(8)

Interaction States

Chain state modifiers to respond to hover, press, focus, and disabled:

RemixButtonStyle()
    .color(Colors.blue)
    .labelColor(Colors.white)
    .onHovered(
      RemixButtonStyle()
          .color(Colors.blue.shade700)
    )
    .onPressed(
      RemixButtonStyle()
          .scale(0.97)
    )
    .onFocused(
      RemixButtonStyle()
          .borderAll(color: Colors.white, width: 2)
    )
    .onDisabled(
      RemixButtonStyle()
          .color(Colors.grey)
          .labelColor(Colors.grey.shade400)
    )

Components that support selection (checkbox, radio, switch, tabs) also have .onSelected():

RemixCheckboxStyle()
    .color(Colors.grey.shade200)
    .onSelected(
      RemixCheckboxStyle()
          .color(Colors.blue)
          .indicatorColor(Colors.white)
    )

Animation

Add smooth transitions between states with .animate():

RemixButtonStyle()
    .color(Colors.blue)
    .onHovered(RemixButtonStyle().color(Colors.blue.shade800))
    .animate(AnimationConfig.spring(300.ms))

AnimationConfig.spring() produces a natural feel. For linear/curve-based animation:

.animate(CurveAnimationConfig(
  duration: Duration(milliseconds: 200),
  curve: Curves.easeOut,
))

Combining Fortal with Overrides

Start from a Fortal preset and layer custom modifications:

final style = FortalButtonStyle.solid()
    .onHovered(RemixButtonStyle().scale(1.02))
    .animate(AnimationConfig.spring(200.ms));

RemixButton(label: 'Click', onPressed: doThing, style: style)

Defining Styles in Widgets

When defining styles within a widget class, use a getter — this is idiomatic Dart for computed properties with no arguments:

class MyScreen extends StatelessWidget {
  RemixButtonStyle get _gradientButtonStyle {
    return RemixButtonStyle.create(
      container: FlexBoxStyler()
        ..gradient.linear(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFF6366F1), Color(0xFFEC4899)],
        ),
    )
        .borderRounded(16)
        .paddingX(32)
        .paddingY(14)
        .labelColor(Colors.white)
        .labelFontSize(16)
        .iconColor(Colors.white)
        .shadow(BoxShadow(
          color: Color(0x406366F1),
          blurRadius: 12,
          offset: Offset(0, 4),
        ))
        .onHovered(RemixButtonStyle().shadow(BoxShadow(
          color: Color(0x80EC4899),
          blurRadius: 28,
          spreadRadius: 4,
        )))
        .onPressed(RemixButtonStyle().scale(0.95))
        .animate(AnimationConfig.spring(250.ms));
  }

  @override
  Widget build(BuildContext context) {
    return RemixButton(
      label: 'Get Started',
      leadingIcon: Icons.rocket_launch,
      onPressed: () {},
      style: _gradientButtonStyle,
    );
  }
}

Callable Styles

Styles with a call() method can be used as functions to create widgets directly:

final primaryButton = RemixButtonStyle()
    .color(Colors.blue)
    .labelColor(Colors.white)
    .paddingX(24)
    .paddingY(12)
    .borderRounded(8);

// Call it to produce a widget:
primaryButton(label: 'Save', onPressed: save)

This works on: RemixButtonStyle, RemixIconButtonStyle, RemixCheckboxStyle, RemixSwitchStyle, RemixRadioStyle, RemixSliderStyle, RemixTextFieldStyle, RemixSelectStyle, RemixMenuStyle, RemixSpinnerStyle.


Fortal Theme System

Fortal is Remix's built-in design system, inspired by Radix UI's color system.

FortalScope

Wrap your widget tree in FortalScope to provide design tokens:

FortalScope(
  accent: FortalAccentColor.blue,   // primary color family
  gray: FortalGrayColor.slate,      // neutral color family
  brightness: Brightness.light,     // light or dark mode
  child: MyApp(),
)

Accent colors (31): amber, blue, bronze, brown, crimson, cyan, gold, grass, green, indigo, iris, jade, lime, mint, orange, pink, plum, purple, red, ruby, sky, teal, tomato, violet, yellow, gray, mauve, slate, sage, olive, sand.

Gray scales (6): gray, mauve, slate, sage, olive, sand.

Using FortalThemeConfig

For programmatic theme control:

final config = FortalThemeConfig(
  accent: FortalAccentColor.purple,
  gray: FortalGrayColor.mauve,
  brightness: Brightness.dark,
);

config.createScope(child: MyApp())

Component Fortal Styles

Every component with Fortal support follows the same pattern:

Fortal<Component>Style.create(
  variant: Fortal<Component>Variant.<name>,
  size: Fortal<Component>Size.<name>,
)

// Or use named constructors for variants:
FortalButtonStyle.solid()
FortalButtonStyle.outline(size: FortalButtonSize.size3)

Fortal Tokens

When building custom styles that should respect the Fortal theme, reference tokens using FortalTokens:

RemixButtonStyle.create(
  container: FlexBoxStyler()
    ..color.ref(FortalTokens.accent9)
    ..borderRadius.ref(FortalTokens.radius3)
    ..padding.vertical.ref(FortalTokens.space2)
    ..padding.horizontal.ref(FortalTokens.space4),
  label: TextStyler()
    ..color.ref(FortalTokens.accentContrast)
    ..style.ref(FortalTokens.text2),
)

Key token families:

  • Colors: accent1accent12, gray1gray12, plus alpha variants (accentA1accentA12, grayA1grayA12)
  • Functional: accentSurface, accentIndicator, accentContrast, colorBackground, colorSurface
  • Space: space1 (4px) through space9 (64px)
  • Radius: radius1 (3px) through radius6 (16px), radiusFull (pill)
  • Typography: text1 (12px) through text9 (60px)
  • Shadows: shadow1 through shadow6

See references/fortal-reference.md for the complete token catalog.


Responsive & Platform Variants

Styles support responsive and platform-aware overrides:

RemixButtonStyle()
    .paddingX(24)
    .onMobile(RemixButtonStyle().paddingX(16).labelFontSize(14))
    .onTablet(RemixButtonStyle().paddingX(20))
    .onDark(RemixButtonStyle().color(Colors.blue.shade800))
    .onLight(RemixButtonStyle().color(Colors.blue))

Available context variants: .onDark(), .onLight(), .onMobile(), .onTablet(), .onDesktop(), .onPortrait(), .onLandscape(), .onIos(), .onAndroid(), .onMacos(), .onWindows(), .onLinux(), .onWeb().


Style Hierarchy Quick Reference

All component styles inherit from a base that determines what convenience methods are available:

  • RemixContainerStyle (box-based): .color(), .padding(), .margin(), .borderRadius(), .border(), .shadow(), .size(), .width(), .height(), .constraints(), .alignment(), .transform(), .scale(), .rotate()
  • RemixFlexContainerStyle (flex-based): everything above + .spacing(), .direction(), .mainAxisAlignment(), .crossAxisAlignment(), .row(), .column()

Plus shared mixins on specific components:

  • LabelStyleMixin: .labelColor(), .labelFontSize(), .labelFontWeight(), .labelFontStyle(), .labelLetterSpacing()
  • IconStyleMixin: .iconColor(), .iconSize(), .iconOpacity()
  • SpinnerStyleMixin: .spinnerIndicatorColor(), .spinnerSize(), .spinnerStrokeWidth()
  • SelectedWidgetStateVariantMixin: .onSelected() (checkbox, radio, switch, tabs)

For deeper Mix-level styling (specs, StyleSpec, BoxStyler, TextStyler, build_runner codegen), consult the Mix skill.


Common Patterns

Reusable Style Tokens

Create a shared style file for your app. Use static getters for styles that chain multiple operations:

class AppStyles {
  static RemixButtonStyle get primaryButton => FortalButtonStyle.solid()
      .animate(AnimationConfig.spring(200.ms));

  static RemixButtonStyle get secondaryButton => FortalButtonStyle.outline()
      .animate(AnimationConfig.spring(200.ms));

  static RemixButtonStyle get dangerButton => FortalButtonStyle.solid()
      .color(Colors.red)
      .onHovered(RemixButtonStyle().color(Colors.red.shade700))
      .animate(AnimationConfig.spring(200.ms));
}

Dark Mode Toggle

class MyApp extends StatefulWidget { ... }

class _MyAppState extends State<MyApp> {
  var _brightness = Brightness.light;

  @override
  Widget build(BuildContext context) {
    return FortalScope(
      accent: FortalAccentColor.indigo,
      brightness: _brightness,
      child: MaterialApp(
        theme: ThemeData(brightness: _brightness),
        home: Scaffold(
          body: Column(children: [
            RemixSwitch(
              selected: _brightness == Brightness.dark,
              onChanged: (val) => setState(() =>
                _brightness = val ? Brightness.dark : Brightness.light),
            ),
            // ... rest of your UI
          ]),
        ),
      ),
    );
  }
}

Form Layout

Column(
  spacing: 16,
  children: [
    RemixTextField(
      controller: nameController,
      label: 'Name',
      hintText: 'Enter your name',
      style: FortalTextFieldStyles.surface(),
    ),
    RemixTextField(
      controller: emailController,
      label: 'Email',
      hintText: 'you@example.com',
      error: emailError,
      style: FortalTextFieldStyles.surface(),
    ),
    RemixSelect<String>(
      trigger: RemixSelectTrigger(placeholder: 'Country'),
      items: countries.map((c) =>
        RemixSelectItem(value: c.code, label: c.name)).toList(),
      selectedValue: selectedCountry,
      onChanged: (val) => setState(() => selectedCountry = val),
    ),
    Row(children: [
      RemixCheckbox(
        selected: agreedToTerms,
        onChanged: (val) => setState(() => agreedToTerms = val ?? false),
      ),
      SizedBox(width: 8),
      Text('I agree to the terms'),
    ]),
    RemixButton(
      label: 'Submit',
      onPressed: agreedToTerms ? handleSubmit : null,
      style: FortalButtonStyle.solid(size: FortalButtonSize.size3),
    ),
  ],
)
Install via CLI
npx skills add https://github.com/btwld/remix --skill remix-ui
Repository Details
star Stars 17
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator