Created
March 9, 2026 15:35
-
-
Save followthemoney1/72222307e75511397688d01c8571108e to your computer and use it in GitHub Desktop.
gradient soft blur effect in flutter. This is how you can use blur effect without frag files and just simple blur effect where on one side its full blurred and on other side of gradient it is not blurred at all
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:ui'; | |
| import 'package:flutter/material.dart'; | |
| void main() { | |
| runApp( | |
| MaterialApp( | |
| home: Scaffold( | |
| body: MyApp(), | |
| ), | |
| ), | |
| ); | |
| } | |
| class MyApp extends StatelessWidget { | |
| final image = 'https://images.unsplash.com/photo-1535892449425-d45bdcb2d016?ixlib=rb-1.2.1&auto=format&fit=crop&w=668&q=80'; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Transform.scale( | |
| scale: 1.2, | |
| child: Stack( | |
| alignment: Alignment.bottomCenter, | |
| fit: StackFit.expand, | |
| children: <Widget>[ | |
| Image.network( | |
| image, | |
| fit: BoxFit.cover, | |
| alignment: Alignment.bottomCenter), | |
| /* BackdropFilter( | |
| filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),*/ | |
| ImageFiltered( | |
| imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), | |
| child: ShaderMask( | |
| shaderCallback: (rect) { | |
| return LinearGradient( | |
| begin: Alignment.topCenter, | |
| end: Alignment.bottomCenter, | |
| colors: [Colors.black, Colors.black.withOpacity(0)], | |
| stops: [0.4, 0.75]).createShader(rect); | |
| }, | |
| blendMode: BlendMode.dstOut, | |
| child: Image.network( | |
| image, | |
| fit: BoxFit.cover, | |
| alignment: Alignment.bottomCenter), | |
| ) | |
| ), | |
| Container( | |
| decoration: BoxDecoration(gradient: LinearGradient( | |
| begin: Alignment.topCenter, | |
| end: Alignment.bottomCenter, | |
| colors: [Colors.black.withOpacity(0), Colors.black.withOpacity(0.6)], | |
| stops: [0.3, 0.8])) | |
| ) | |
| ])); | |
| } | |
| } |
Author
followthemoney1
commented
Mar 9, 2026
Author
This implementation βπΏ requires having an image or any kind of surface to make it blur. Color is not working unfortunatly
Author
instead you can use this implementation:
/// A widget that applies a progressive (variable) blur effect over its child.
///
/// The blur strength increases gradually along the specified [direction],
/// creating the "scroll edge fade" effect popularized by iOS Maps, App Library, etc.
///
/// ## How it works
/// Internally, this divides the blur region into [steps] thin slices, each with
/// a linearly increasing `sigmaX`/`sigmaY` from 0 to [maxSigma]. With enough
/// steps (default 20), the transition appears smooth and continuous.
///
/// Optionally, a gradient overlay fades to [overlayColor] at the blur's
/// strongest edge to mask any visual artifacts (recommended).
///
/// ## Usage
/// ```dart
/// ProgressiveBlur(
/// maxSigma: 20,
/// blurExtent: 120,
/// direction: ProgressiveBlurDirection.bottomToTop, // blur grows upward
/// child: ListView(
/// children: [ /* your content */ ],
/// ),
/// )
/// ```
class ProgressiveBlur extends StatelessWidget {
const ProgressiveBlur({
super.key,
required this.child,
this.maxSigma = 16.0,
this.blurExtent = 100.0,
this.steps = 20,
this.direction = ProgressiveBlurDirection.bottomToTop,
this.overlayColor,
this.overlayOpacity = 0.6,
});
/// The content underneath the blur effect.
final Widget child;
/// Maximum blur sigma at the strongest edge.
final double maxSigma;
/// The height (or width, for horizontal directions) of the blur region in logical pixels.
final double blurExtent;
/// Number of slices used to approximate the progressive blur.
/// Higher = smoother but more layers. 20 is a good default.
final int steps;
/// Direction in which blur strength increases.
final ProgressiveBlurDirection direction;
/// Optional solid color for the gradient overlay that hides edge artifacts.
/// Pass `null` to disable the overlay. Defaults to `null` (auto-detects from
/// the nearest [Scaffold] background or falls back to white).
final Color? overlayColor;
/// Opacity of the gradient overlay at its strongest point. 0.0β1.0.
final double overlayOpacity;
@override
Widget build(BuildContext context) {
final effectiveOverlayColor = overlayColor ?? Theme.of(context).scaffoldBackgroundColor;
return Stack(
children: [
// 1. The actual content
child,
// 2. The progressive blur slices
Positioned.fill(
child: IgnorePointer(
child: _buildBlurStack(),
),
),
// 3. Optional gradient overlay to clean up the strongest edge
if (overlayOpacity > 0)
Positioned.fill(
child: IgnorePointer(
child: _buildGradientOverlay(effectiveOverlayColor),
),
),
],
);
}
Widget _buildBlurStack() {
return LayoutBuilder(
builder: (context, constraints) {
final isVertical =
direction == ProgressiveBlurDirection.topToBottom || direction == ProgressiveBlurDirection.bottomToTop;
final totalSize = isVertical ? constraints.maxHeight : constraints.maxWidth;
final clampedExtent = blurExtent.clamp(0.0, totalSize);
return Stack(
children: List.generate(steps, (i) {
// Each slice gets a linearly increasing sigma.
// Slice 0 = no blur, slice (steps-1) = maxSigma.
final t = i / (steps - 1).clamp(1, steps);
final sigma = maxSigma * t;
if (sigma < 0.1) return const SizedBox.shrink();
final sliceStart = clampedExtent * (i / steps);
final sliceEnd = clampedExtent * ((i + 1) / steps);
final sliceSize = sliceEnd - sliceStart;
return _positionedSlice(
totalSize: totalSize,
clampedExtent: clampedExtent,
sliceStart: sliceStart,
sliceSize: sliceSize,
sigma: sigma,
isVertical: isVertical,
);
}),
);
},
);
}
Widget _positionedSlice({
required double totalSize,
required double clampedExtent,
required double sliceStart,
required double sliceSize,
required double sigma,
required bool isVertical,
}) {
// Calculate the offset from the "blur edge" of the container.
// For bottomToTop: slices start from the bottom.
// For topToBottom: slices start from the top.
final double offset = sliceStart;
final alignment = _edgeAlignment;
if (isVertical) {
return Positioned(
left: 0,
right: 0,
top: direction == ProgressiveBlurDirection.topToBottom ? offset : null,
bottom: direction == ProgressiveBlurDirection.bottomToTop ? offset : null,
height: sliceSize + 1, // +1 to avoid subpixel gaps
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: const SizedBox.expand(),
),
),
);
} else {
return Positioned(
top: 0,
bottom: 0,
left: direction == ProgressiveBlurDirection.leftToRight ? offset : null,
right: direction == ProgressiveBlurDirection.rightToLeft ? offset : null,
width: sliceSize + 1,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: const SizedBox.expand(),
),
),
);
}
}
Alignment get _edgeAlignment {
switch (direction) {
case ProgressiveBlurDirection.topToBottom:
return Alignment.topCenter;
case ProgressiveBlurDirection.bottomToTop:
return Alignment.bottomCenter;
case ProgressiveBlurDirection.leftToRight:
return Alignment.centerLeft;
case ProgressiveBlurDirection.rightToLeft:
return Alignment.centerRight;
}
}
Widget _buildGradientOverlay(Color color) {
final begin = _gradientBegin;
final end = _gradientEnd;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: begin,
end: end,
colors: [
color.withValues(alpha: 0),
color.withValues(alpha: overlayOpacity),
],
// The overlay gradient covers the same extent as the blur region.
// It starts transparent and ends at overlayOpacity.
stops: const [0.0, 1.0],
),
),
child: const SizedBox.expand(),
);
}
Alignment get _gradientBegin {
switch (direction) {
case ProgressiveBlurDirection.topToBottom:
return Alignment.bottomCenter; // transparent at bottom
case ProgressiveBlurDirection.bottomToTop:
return Alignment.topCenter; // transparent at top
case ProgressiveBlurDirection.leftToRight:
return Alignment.centerRight;
case ProgressiveBlurDirection.rightToLeft:
return Alignment.centerLeft;
}
}
Alignment get _gradientEnd {
switch (direction) {
case ProgressiveBlurDirection.topToBottom:
return Alignment.topCenter;
case ProgressiveBlurDirection.bottomToTop:
return Alignment.bottomCenter;
case ProgressiveBlurDirection.leftToRight:
return Alignment.centerLeft;
case ProgressiveBlurDirection.rightToLeft:
return Alignment.centerRight;
}
}
}
/// Direction in which the blur strength increases.
enum ProgressiveBlurDirection {
/// Blur grows from bottom (none) β top (max). Good for bottom bars.
topToBottom,
/// Blur grows from top (none) β bottom (max). Good for top bars / status bars.
bottomToTop,
/// Blur grows from right (none) β left (max).
leftToRight,
/// Blur grows from left (none) β right (max).
rightToLeft,
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment