Skip to content

[cupertino_ui] Dim disabled CupertinoSlider controls - #13419

Merged
auto-submit[bot] merged 3 commits into
flutter:mainfrom
Wreos:fix/cupertino-slider-disabled-appearance
Nov 11, 2026
Merged

auto-submit[bot] merged 3 commits into
flutter:mainfrom
Wreos:fix/cupertino-slider-disabled-appearance

Conversation

@Wreos

@Wreos Wreos commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Dims CupertinoSlider to 50% opacity when onChanged is null, matching the disabled appearance in native UIKit Slider component on iOS. Adds light/dark regression tests for disabling and re-enabling the slider.

Fixes flutter/flutter#106691.

Screenshots

Flutter before/after: iPhone 16 Pro simulator, iOS 18.6, light mode, value 0.67. The top slider is enabled; the bottom is disabled. The native reference shows stock UISlider controls in both themes.

Before After Native UIKit reference
Before: enabled and disabled sliders look identical After: disabled slider is dimmed Native UIKit: disabled sliders are dimmed in light and dark mode

Also checked native UIKit on iOS 26.5, where the disabled visual element uses 0.5 opacity. This fixes disabled styling, not the slider's overall design.

Validation: all 23 tests in test/slider_test.dart pass; targeted analysis and formatting pass. The full package suite has not been run locally. No public API changes.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2

@github-actions github-actions Bot added p: cupertino_ui triage-design Should be looked at in design triage labels Oct 25, 2026
@Wreos
Wreos marked this pull request as ready for review October 25, 2026 11:21

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates CupertinoSlider to dim when disabled by wrapping it in an Opacity widget, matching the native iOS appearance. It also adds tests to verify the visual and functional behavior of the disabled slider and updates existing semantics tests. The review feedback suggests conditionally applying the Opacity widget only when the slider is disabled to avoid unnecessary widget tree overhead, and updating the tests to assert the absence of the Opacity widget when the slider is enabled.

Comment on lines +298 to 313
return Opacity(
opacity: widget.onChanged == null ? _kDisabledOpacity : 1.0,
child: _CupertinoSliderRenderObjectWidget(
value: (widget.value - widget.min) / (widget.max - widget.min),
divisions: widget.divisions,
activeColor: CupertinoDynamicColor.resolve(
widget.activeColor ?? CupertinoTheme.of(context).primaryColor,
context,
),
thumbColor: widget.thumbColor,
onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
vsync: this,
),
thumbColor: widget.thumbColor,
onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
vsync: this,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Wrapping the slider in an Opacity widget when it is enabled (i.e., onChanged != null) introduces unnecessary overhead in the widget tree. In Flutter, it is a best practice to conditionally wrap widgets in Opacity only when the opacity is actually less than 1.0 (similar to how CupertinoButton handles disabledOpacity). This keeps the widget tree shallower and avoids any potential performance cost of the Opacity widget during normal enabled use.

    final Widget result = _CupertinoSliderRenderObjectWidget(
      value: (widget.value - widget.min) / (widget.max - widget.min),
      divisions: widget.divisions,
      activeColor: CupertinoDynamicColor.resolve(
        widget.activeColor ?? CupertinoTheme.of(context).primaryColor,
        context,
      ),
      thumbColor: widget.thumbColor,
      onChanged: widget.onChanged != null ? _handleChanged : null,
      onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
      onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
      vsync: this,
    );

    if (widget.onChanged == null) {
      return Opacity(
        opacity: _kDisabledOpacity,
        child: result,
      );
    }

    return result;

@Wreos Wreos Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the wrapper here. Opacity at 1.0 already uses a fast path without an intermediate buffer. Adding and removing it would recreate the slider render object whenever enabled changes. Flutter also recommends keeping the subtree structure stable, and CupertinoSwitch uses the same approach.

I added checks in 4b4cde2 to verify that the render object stays the same after disabling and re-enabling, in both themes.

Comment on lines +56 to +77
final Finder opacity = find.descendant(
of: find.byType(CupertinoSlider),
matching: find.byType(Opacity),
);
expect(tester.widget<Opacity>(opacity).opacity, 1.0);
final Size enabledSize = tester.getSize(find.byType(CupertinoSlider));

updateState(() => enabled = false);
await tester.pump();
expect(tester.widget<Opacity>(opacity).opacity, 0.5);
expect(tester.getSize(find.byType(CupertinoSlider)), enabledSize);
final CupertinoSlider slider = tester.widget<CupertinoSlider>(find.byType(CupertinoSlider));
expect(slider.activeColor, CupertinoColors.systemPurple);
expect(slider.thumbColor, CupertinoColors.systemOrange);
await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
expect(value, 0.5);

updateState(() => enabled = true);
await tester.pump();
expect(tester.widget<Opacity>(opacity).opacity, 1.0);
await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
expect(value, greaterThan(0.5));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the Opacity widget is now conditionally applied only when the slider is disabled, update the tests to assert that no Opacity widget is present when the slider is enabled.

      expect(find.byType(Opacity), findsNothing);
      final Size enabledSize = tester.getSize(find.byType(CupertinoSlider));

      updateState(() => enabled = false);
      await tester.pump();
      final Finder opacity = find.descendant(
        of: find.byType(CupertinoSlider),
        matching: find.byType(Opacity),
      );
      expect(tester.widget<Opacity>(opacity).opacity, 0.5);
      expect(tester.getSize(find.byType(CupertinoSlider)), enabledSize);
      final CupertinoSlider slider = tester.widget<CupertinoSlider>(find.byType(CupertinoSlider));
      expect(slider.activeColor, CupertinoColors.systemPurple);
      expect(slider.thumbColor, CupertinoColors.systemOrange);
      await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
      expect(value, 0.5);

      updateState(() => enabled = true);
      await tester.pump();
      expect(find.byType(Opacity), findsNothing);
      await tester.dragFrom(tester.getCenter(find.byType(CupertinoSlider)), const Offset(50, 0));
      expect(value, greaterThan(0.5));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depends on the conditional-wrapper suggestion above, which I have not adopted. The tests continue checking opacity 1.0 / 0.5 / 1.0 and now also check that the slider render object is preserved across both transitions. Slider tests, targeted analysis, and formatting pass.

@dkwingsmt dkwingsmt left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you for the fix (and the well-written description)!

@dkwingsmt
dkwingsmt requested a review from pjkammer November 10, 2026 07:49
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Nov 11, 2026
@pjkammer pjkammer added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Nov 11, 2026
@auto-submit
auto-submit Bot merged commit e81cfde into flutter:main Nov 11, 2026
14 checks passed
b-luk pushed a commit to b-luk/flutter that referenced this pull request Nov 12, 2026
…r#196450)

flutter/packages@653628e...14f9933

2026-11-12 [email protected] [material_ui]
Fix SnackBarThemeData.lerp dropping showCloseIcon
(flutter/packages#13449)
2026-11-11 [email protected] [ci] Update repo
for 3.50 stable release (flutter/packages#13628)
2026-11-11 [email protected] [vector_graphics]
Use font-family lists as a family with fallbacks
(flutter/packages#13552)
2026-11-11 [email protected] Roll Flutter (stable) from
c197ca5 to ab29120 (1647 revisions) (flutter/packages#13625)
2026-11-11 [email protected] [shared_prferences] Adopts
mockito-kotlin in tests (flutter/packages#13624)
2026-11-11 [email protected] [cupertino_ui] Dim disabled
CupertinoSlider controls (flutter/packages#13419)
2026-11-11 [email protected] [flutter_svg] Invalidate pending cache
entries on clear and evict (flutter/packages#13595)
2026-11-11 [email protected] [cupertino_ui] Fix the tinted
button's fill opacity (flutter/packages#13436)
2026-11-11 [email protected] [various]
Support SVG filters (blur, offset, feMerge) (flutter/packages#11909)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: cupertino_ui triage-design Should be looked at in design triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CupertinoSlider with null onChanged doesn't display as disabled

3 participants