Flutter package
flutter_shaders_ui
GPU-accelerated GLSL shader effects for Flutter UI — ten drop-in widgets, zero dependencies.
Custom shaders
Build your own shader widgets on the core API: ShaderEffectWidget, ShaderCache and ShaderPainter.
source: README.mdCore API
The core API
Every built-in effect is a thin wrapper over three public classes. Build your own widget by loading a .frag asset through the same pipeline and feeding it uniforms.
| Class | Purpose |
|---|---|
ShaderEffectWidget | Base widget: shader loading, time animation, uniform setup |
ShaderCache | Global cache for compiled FragmentProgram instances |
ShaderPainter | Reusable CustomPainter for rendering shaders to canvas |
ShaderEffectWidget
The base widget for rendering animated GLSL fragment shaders. It manages shader loading (via ShaderCache), time animation and size. Point it at a bundled asset and, optionally, a uniformSetter to feed your own parameters.
ShaderEffectWidget(
assetPath: 'packages/flutter_shaders_ui/shaders/snow.frag',
child: Text('Hello'),
)const ShaderEffectWidget({
super.key,
required this.assetPath,
this.child,
this.uniformSetter,
this.enabled = true,
this.showAsOverlay = false,
});| Parameter | Type | Default | Description |
|---|---|---|---|
assetPath | String | required | Path to the .frag shader asset |
child | Widget? | null | Optional child widget. Rendered behind or under the shader effect |
uniformSetter | ShaderUniformSetter? | null | Callback to set custom uniforms beyond the standard ones |
enabled | bool | true | Whether the shader is active. When false, only child renders |
showAsOverlay | bool | false | If true, shader renders as overlay on top of child. If false (default), shader renders as background behind child |
The uniformSetter runs each frame. It receives the shader, the current size, the elapsed time and the starting float index, and must return the next free index.
/// Callback to configure shader uniforms each frame.
///
/// - [shader]: the fragment shader instance
/// - [size]: current widget size in logical pixels
/// - [time]: elapsed time in seconds
/// - [index]: starting uniform float index (after standard uniforms)
///
/// Must return the next available uniform index.
typedef ShaderUniformSetter = int Function(
ui.FragmentShader shader,
Size size,
double time,
int index,
);Heads up
Internally the widget drives time with a Ticker and wraps the paint in RepaintBoundary + SizedBox.expand + CustomPaint(willChange: true). When enabled is false or the program has not loaded yet, it returns child ?? const SizedBox.shrink() — the zero-cost disabled guarantee.
Uniforms & shader assets
Every shader receives the same standard uniforms first. Your custom uniforms start at index 3, which is exactly the index handed to your uniformSetter.
| Index | GLSL uniform | Description |
|---|---|---|
0-1 | vec2 uResolution | Widget width & height |
2 | float uTime | Elapsed seconds |
Shaders ship inside the package and are declared in pubspec.yaml. Reference them by their package path (for example packages/flutter_shaders_ui/shaders/aurora.frag) so they resolve correctly on pub.dev.
flutter:
shaders:
- shaders/aurora.frag
- shaders/fire.frag
- shaders/glass.frag
- shaders/glow_orb.frag
- shaders/pulse.frag
- shaders/ripple.frag
- shaders/shimmer.frag
- shaders/snow.frag
- shaders/water.frag
- shaders/wave.fragA custom effect end-to-end
The package's own WaterEffect.build is the canonical pattern: load the asset, set every custom uniform relative to i, and return the next index. Here eleven floats (starting at index 3) carry speed, four clamped scalars and the RGB channels of both colors.
@override
Widget build(BuildContext context) {
return ShaderEffectWidget(
assetPath: _assetPath,
enabled: enabled,
uniformSetter: (shader, size, time, i) {
shader.setFloat(i, speed);
shader.setFloat(i + 1, depth.clamp(0.0, 1.0));
shader.setFloat(i + 2, waveIntensity.clamp(0.0, 1.0));
shader.setFloat(i + 3, causticIntensity.clamp(0.0, 1.0));
shader.setFloat(i + 4, foamAmount.clamp(0.0, 1.0));
shader.setFloat(i + 5, color1.r);
shader.setFloat(i + 6, color1.g);
shader.setFloat(i + 7, color1.b);
shader.setFloat(i + 8, color2.r);
shader.setFloat(i + 9, color2.g);
shader.setFloat(i + 10, color2.b);
return i + 11;
},
child: child,
);
}Where static const _assetPath = 'packages/flutter_shaders_ui/shaders/water.frag';
ShaderCache
A global cache for loaded ui.FragmentProgram instances. It prevents recompilation by caching programs by asset path, and is thread-safe for single-isolate Flutter usage.
final program = await ShaderCache.load('shaders/snow.frag');
final shader = program.fragmentShader();static Future<ui.FragmentProgram> load(String assetPath)
static void evict(String assetPath)
static void clearAll()| Method | Description |
|---|---|
load | Loads a ui.FragmentProgram from the given assetPath, using cache. If the shader is already loaded, returns the cached instance. If currently loading, awaits the same future (deduplication) |
evict | Evicts a specific shader from the cache |
clearAll | Clears all cached shaders |
ShaderPainter
A reusable CustomPainter that renders a ui.FragmentShader. The shader must have its uniforms set before painting; the painter simply draws a rect filled with the shader.
ShaderPainter({
required this.shader,
super.repaint,
});| Parameter | Type | Description |
|---|---|---|
shader | ui.FragmentShader | The fragment shader to render |
repaint | Listenable? (inherited) | If repaint is provided, the painter repaints on each tick |
Performance & caching
The same guarantees that make the built-in effects cheap apply to any widget you build on the core API.
- Compiled once — ShaderCache.load caches every FragmentProgram by asset path and deduplicates concurrent loads, so a shader is never compiled twice.
- Zero cost when disabled — with enabled: false, or before the program finishes loading, the widget returns only its child (or SizedBox.shrink) and does no GPU work.
- Isolated repaints — the shader paint is wrapped in a RepaintBoundary so shader frames do not repaint the surrounding widget tree.
Let's make something together.
If you have any questions about a new project or other inquiries, feel free to contact us. We will get back to you as soon as possible.
