Skip to content

Ios17Header

Ios17Header is a Cupertino-style indicator that recreates the native iOS 17 pull-to-refresh experience. It renders a 12-spoke tick spinner whose ticks progressively appear while pulling, then rotate with a comet-tail gradient when refreshing. On physical iOS devices, it fires a HapticFeedback.mediumImpact() at the moment the threshold is crossed.


SmartRefresher(
controller: _controller,
header: const Ios17Header(),
onRefresh: _onRefresh,
child: ListView.builder(...),
)

PropertyTypeDefaultDescription
colorColor?CupertinoColors.systemFillTint color of the 12 tick marks.
radiusdouble10.0Radius of the spinner (distance from center to tip of tick).
showLastUpdatedboolfalseWhen true, shows a timestamp below the indicator after a successful refresh.
enableHapticbooltrueFires HapticFeedback.mediumImpact() when the refresh threshold is crossed (iOS only).
lastUpdatedTextBuilderString Function(DateTime)?nullCustom formatter for the “Updated X min ago” text.
heightdouble60.0Height reserved for the header.
completeDurationDuration300msHow long the completed state is shown before dismissal.
refreshStyleRefreshStyle.followThe indicator follows the list scroll.
semanticsLabelString?Localized stringAccessibility label for screen readers.
semanticsHintString?nullAccessibility hint.

Ios17Header(
showLastUpdated: true,
// Optional custom text format
lastUpdatedTextBuilder: (updatedAt) {
final diff = DateTime.now().difference(updatedAt);
if (diff.inSeconds < 60) return 'Just now';
return '${diff.inMinutes}m ago';
},
)

The timestamp appears below the spinner once refreshCompleted() is called, then fades out during the dismiss animation.


Haptic feedback fires at the moment the pull threshold is crossed (from canRefresh state inwards to refreshing). It uses HapticFeedback.mediumImpact() and is only functional on physical iOS devices.

// Disable haptics (e.g. for accessibility preferences):
Ios17Header(enableHaptic: false)
// Enable globally via RefreshConfiguration:
RefreshConfiguration(
enableThresholdHaptic: true,
child: MaterialApp(...),
)

The indicator is composed of four AnimationControllers:

ControllerDurationPurpose
_rotationController1000ms, repeatingDrives the comet-tail rotation during active refresh.
_scaleController200ms, elastic springProduces the “pop” scale effect when the threshold is crossed.
_opacityController300msFades in the spinning gradient when refreshing begins.
_dismissController300msScales and fades the spinner out on completion.

The 12 ticks are drawn via a CustomPainter (_ActivityIndicatorPainter). Each tick is a rounded rectangle. Their alpha values are calculated per-tick based on progress and rotationValue:

  • While pulling (progress < 1.0): Ticks appear sequentially from the top, one per 1/12 of pull distance.
  • While spinning (progress ≥ 1.0): The leading tick is 255 alpha; each subsequent tick in the comet tail uses [255, 220, 184, 148, 112, 76, 47, 47, ...] alpha values, giving the trailing fade effect.

Ios17ActivityIndicator is a reusable public widget if you want to embed the iOS 17 spinner anywhere else in your UI:

Ios17ActivityIndicator(
color: CupertinoColors.systemGrey,
radius: 12.0,
progress: 0.75, // how many ticks are visible (0.0–1.0 while pulling)
rotationValue: 0.3, // spinner rotation (0.0–1.0, where 1.0 = full turn)
gradientOpacity: 1.0, // intensity of the comet-tail gradient
)

Both Ios17Header and Ios17ActivityIndicator expose semanticsLabel and semanticsHint. The header’s label transitions through localized strings for each RefreshStatus.

Ios17Header(
semanticsLabel: 'Pull down to reload messages',
)