Open Source

Flutter package

flutter_shaders_ui

GPU-accelerated GLSL shader effects for Flutter UI — ten drop-in widgets, zero dependencies.

v1.1.223.2k / month40 likesMIT10 effectsFlutter SDK only

Custom shaders

Build your own shader widgets on the core API: ShaderEffectWidget, ShaderCache and ShaderPainter.

source: README.md

Core 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.

ClassPurpose
ShaderEffectWidgetBase widget: shader loading, time animation, uniform setup
ShaderCacheGlobal cache for compiled FragmentProgram instances
ShaderPainterReusable 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.

Minimal usagedart
ShaderEffectWidget(
  assetPath: 'packages/flutter_shaders_ui/shaders/snow.frag',
  child: Text('Hello'),
)
Constructordart
const ShaderEffectWidget({
  super.key,
  required this.assetPath,
  this.child,
  this.uniformSetter,
  this.enabled = true,
  this.showAsOverlay = false,
});
ParameterTypeDefaultDescription
assetPathStringrequiredPath to the .frag shader asset
childWidget?nullOptional child widget. Rendered behind or under the shader effect
uniformSetterShaderUniformSetter?nullCallback to set custom uniforms beyond the standard ones
enabledbooltrueWhether the shader is active. When false, only child renders
showAsOverlayboolfalseIf 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.

ShaderUniformSetterdart
/// 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.

IndexGLSL uniformDescription
0-1vec2 uResolutionWidget width & height
2float uTimeElapsed 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.

pubspec.yamlyaml
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.frag

A 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.

WaterEffect.build()dart
@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.

Load a programdart
final program = await ShaderCache.load('shaders/snow.frag');
final shader = program.fragmentShader();
Method signaturesdart
static Future<ui.FragmentProgram> load(String assetPath)
static void evict(String assetPath)
static void clearAll()
MethodDescription
loadLoads 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)
evictEvicts a specific shader from the cache
clearAllClears 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.

Constructordart
ShaderPainter({
  required this.shader,
  super.repaint,
});
ParameterTypeDescription
shaderui.FragmentShaderThe fragment shader to render
repaintListenable? (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.

Codigee
We are using cookies. Learn more