veil
Selectively apply animated visual effects to a Flutter widget subtree, with per-child opt-out via Unveiled.
Preview

Features
- Animated greyscale — smooth toggle with configurable duration and curve
- Colour overlay — dim or tint on top of the greyscale, also animated
- Selective opt-out — wrap any descendant in
Unveiledto keep it full-colour and undimmed, even mid-animation - Configurable overlay colour — black dim, warm amber, cool blue, anything
- Crash-safe — uses Flutter compositing primitives (
pushColorFilter,pushOpacity,pushClipRect), never rawcanvas.saveLayer/restore - Zero GC pressure — pre-allocated matrix buffer, cached
ColorFilterandPaintper render object - Assertion-safe —
isRepaintBoundaryis permanentlytrue, satisfying Flutter'sPipelineOwner.flushPaintinvariant
Getting started
dependencies:
veil: ^1.0.0
import 'package:veil/veil.dart';
Usage
Basic greyscale
Veil(
enable: isSoldOut,
child: ProductCard(),
)
Partial greyscale
Veil(
enable: isDisabled,
greyOpacity: 0.6, // 60% grey, 40% colour retained
child: ProductCard(),
)
With overlay
Veil(
enable: isSoldOut,
greyOpacity: 1.0,
overlayOpacity: 0.35,
overlayColor: Colors.black, // default
child: ProductCard(),
)
Custom tint colour
// Warm amber — "out of season"
Veil(
enable: isOffSeason,
greyOpacity: 0.6,
overlayOpacity: 0.25,
overlayColor: Colors.orange,
child: ProductCard(),
)
// Cool blue — "coming soon"
Veil(
enable: isComingSoon,
greyOpacity: 0.8,
overlayOpacity: 0.3,
overlayColor: Color(0xFF1A237E),
child: ProductCard(),
)
Selective opt-out with Unveiled
Veil(
enable: isSoldOut,
greyOpacity: 1.0,
overlayOpacity: 0.35,
child: ProductCard(
child: Column(
children: [
ProductImage(), // greyscale + dimmed ✓
ProductTitle(), // greyscale + dimmed ✓
Unveiled( // full colour, not dimmed ✓
child: PriceTag(),
),
Unveiled( // full colour, not dimmed ✓
child: AddToCartButton(),
),
],
),
),
)
Custom animation
Veil(
enable: isDisabled,
duration: Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
child: Card(),
)
API reference
Veil
| Property | Type | Default | Description |
|---|---|---|---|
child |
Widget |
required | The subtree to apply effects to |
enable |
bool |
true |
Activates the veil effects |
greyOpacity |
double |
1.0 |
Greyscale intensity (0.0–1.0) |
overlayOpacity |
double |
0.0 |
Overlay opacity (0.0–1.0) |
overlayColor |
Color |
Color(0xFF000000) |
Overlay tint colour (alpha ignored) |
duration |
Duration |
350ms |
Toggle animation duration |
curve |
Curve |
Curves.easeInOut |
Toggle animation curve |
Unveiled
| Property | Type | Description |
|---|---|---|
child |
Widget |
Widget to exempt from all Veil effects |
How it works
Veil uses a custom RenderObject (RenderVeil) that paints in three steps:
- Greyscale — the entire subtree is painted through a
ColorFilterLayerusing the ITU-R BT.709 luminance matrix - Overlay — a solid colour rect is painted at the configured opacity via
OpacityLayer - Unveiled overdraw — each
Unveiledchild is re-painted on top of both layers — unfiltered and undimmed — clipped to its own bounds
All compositing uses Flutter's own layer primitives rather than raw canvas.saveLayer / restore, which prevents the "native peer has been collected" crash that occurs when a child RepaintBoundary is composited mid-paint.
License
This package is released under the MIT License.
Libraries
- veil
- veil — selectively apply animated visual effects to a Flutter widget
subtree, with per-child opt-out via
Unveiled.