Free copy-paste Loader 011 for Flutter: a ready-to-use loader widget with full Dart source code and a live preview. Works on mobile & desktop. No external packages.
Copy the Dart code below into a new file in your Flutter project (for example lib/widgets/loader_011.dart) and use the widget anywhere in your widget tree.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Loader011 extends StatefulWidget {
const Loader011({
super.key,
this.text = 'LOADING',
this.textColor = Colors.white,
this.accentColor = const Color(0xFF888888),
this.duration = const Duration(milliseconds: 1000),
});
final String text;
final Color textColor;
final Color accentColor;
final Duration duration;
@override
State<Loader011> createState() => _Loader011State();
}
class _Loader011State extends State<Loader011>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
static const List<double> _fontScales = [
0.50,
0.62,
0.77,
0.91,
1.00,
0.91,
0.77,
0.62,
0.50,
];
static const List<double> _xOffsets = [
-54.0,
-26.0,
-9.0,
-1.5,
0.0,
1.5,
9.0,
26.0,
54.0,
];
static const List<double> _opacities = [
0.55,
0.68,
0.78,
0.90,
1.00,
0.90,
0.78,
0.68,
0.55,
];
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration)
..repeat();
}
@override
void didUpdateWidget(covariant Loader011 oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller.duration = widget.duration;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
final t = _controller.value;
final wobble = math.sin(t * 2 * math.pi);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 140,
height: 32,
child: Stack(
alignment: Alignment.center,
children: List.generate(9, (i) {
final leftBound = i / 9.0;
final rightBound = (i + 1) / 9.0;
final scale = _fontScales[i];
final x = _xOffsets[i];
final op = _opacities[i];
final scrollT = (t * 2.0 - 1.0);
final textShift = scrollT * 60.0;
return Positioned(
child: Transform.translate(
offset: Offset(x, 0),
child: Opacity(
opacity: op,
child: ClipRect(
clipper: _SliceClipper(leftBound, rightBound),
child: Transform.translate(
offset: Offset(textShift, 0),
child: Text(
widget.text,
style: TextStyle(
color: widget.textColor,
fontSize: 18 * scale,
fontWeight: FontWeight.w900,
letterSpacing: 2.0,
),
),
),
),
),
),
);
}),
),
),
const SizedBox(height: 8),
Container(
width: 44,
height: 3,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: widget.textColor.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment(wobble, 0.0),
widthFactor: 0.35,
child: Container(
decoration: BoxDecoration(
color: widget.textColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
},
);
}
}
class _SliceClipper extends CustomClipper<Rect> {
const _SliceClipper([this.startFrac = 0.0, this.endFrac = 1.0]);
final double startFrac;
final double endFrac;
@override
Rect getClip(Size size) {
return Rect.fromLTRB(
size.width * startFrac,
0,
size.width * endFrac,
size.height,
);
}
@override
bool shouldReclip(_SliceClipper oldClipper) =>
oldClipper.startFrac != startFrac || oldClipper.endFrac != endFrac;
}