RemixRemix

Styling components

Define appearance, respond to interaction states, and reuse Remix styles

Start a style with the component's concrete styler and chain the properties you need. Keep application values and callbacks on the widget; put appearance in the styler. This is the same separation used throughout Mix.

Respond to interaction states

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

final buttonStyle = ButtonStyler()
    .padding(.horizontal(16))
    .padding(.vertical(10))
    .color(Colors.blue)
    .borderRadius(.circular(8))
    .label(.color(Colors.white))
    .onHovered(.color(Colors.blue.shade700))
    .onPressed(.scale(0.95));

Remix tracks hover and press for the component. The state-specific styles describe the appearance to apply; you do not need another hover detector to drive these variants. Use the component reference for its disabled, loading, and focus behavior.

Animate style changes

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

final animatedButton = ButtonStyler()
    .color(Colors.blue)
    .onHovered(.color(Colors.blue.shade700))
    .onPressed(.scale(0.95))
    .animate(AnimationConfig.spring(300.ms));

The animation applies to style transitions, including the color and scale in this example. It does not supply application behavior such as saving data or deciding when a loading state begins.

Reduced motion

Remix reads two signals. MediaQuery.disableAnimations is set by Android and desktop settings and can be overridden for a subtree. The platform's accessibility reduceMotion is the only flag iOS Reduce Motion sets. Motion is reduced when either is on. A MediaQuery override can force reduced motion on for a subtree, but it cannot turn the platform's reduceMotion off.

The select menu, accordion panel, skeleton, and sidebar follow both signals. An active select or accordion transition settles when the preference changes; essential spinner motion keeps running. Dialogs, tooltips, disclosure, and toast get their motion from Naked UI, so they follow whichever signals the installed Naked UI release reads.

Read the same preference for your own motion with RemixReducedMotionBuilder. Its builder runs again when either signal changes.

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

Widget fadingPanel({required bool open, required Widget child}) {
  return RemixReducedMotionBuilder(
    builder: (context, reduceMotion) => AnimatedOpacity(
      opacity: open ? 1 : 0,
      duration: reduceMotion
          ? Duration.zero
          : const Duration(milliseconds: 300),
      child: child,
    ),
  );
}

Style animations declared with .animate(...), including those in installed presets, are driven by Mix, which never reads reduced motion and gives Remix no point to intercept it. Flutter already shortens Mix's curve animations while the platform's disableAnimations is set. Springs, iOS Reduce Motion, and a MediaQuery override leave them running. Choose the animation yourself while reduceMotion is true. Mix keeps the old animation driver while a style is moving, so dropping .animate takes effect once the style settles:

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

Widget saveButton(VoidCallback save) {
  final base = ButtonStyler()
      .color(Colors.blue)
      .onHovered(.color(Colors.blue.shade700));

  return RemixReducedMotionBuilder(
    builder: (context, reduceMotion) => RemixButton(
      label: 'Save',
      onPressed: save,
      style: reduceMotion
          ? base
          : base.animate(AnimationConfig.spring(300.ms)),
    ),
  );
}

Reuse a base style

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

final baseButton = ButtonStyler()
    .padding(.horizontal(16))
    .padding(.vertical(10))
    .borderRadius(.circular(8));

final primaryButton = baseButton
    .color(Colors.blue)
    .label(.color(Colors.white));

final destructiveButton = baseButton
    .color(Colors.red)
    .label(.color(Colors.white));

Each chain returns a new style, leaving baseButton unchanged. Both variants retain the shared spacing and radius while defining their own colors.

Choose between a style and an installed recipe

Use a styler directly when the application only needs a reusable appearance. Use Open Code when you want a local token system, named component variants, and generated widget adapters that your team can maintain.

Follow the Styler API for method names and contextual shorthand. Use canonical calls such as .padding(.horizontal(16)) for spacing.

View page source on GitHub