shader_transitions 0.3.0 copy "shader_transitions: ^0.3.0" to clipboard
shader_transitions: ^0.3.0 copied to clipboard

GPU-accelerated shader page transitions for Flutter — diamond, circle iris, and wipe with eight sweep directions, optional cover color, and a cinematic hold.

shader_transitions #

pub package License: MIT Flutter

GPU-accelerated, shader-based page transitions for Flutter — diamond grid, circle iris, and linear wipe with eight sweep directions, optional cover color, and a configurable hold for cinematic fade-through transitions.

hero


Features #

  • Three shader transitions out of the box — diamond grid, circle iris, wipe linear.
  • Eight sweep directions — four axis-aligned, four diagonals; all reach every corner regardless of direction.
  • Optional cover color — replace the cross-fade through the outgoing page with a flat color (e.g. black) for a true cinematic fade.
  • Configurable cover hold — pause on a full-cover frame between the cover-in and page-in wipes; pass a Duration.
  • Drop-in for Navigator, go_router, and auto_routeShaderPageRoute for Navigator, ShaderTransitionBuilders.create(config) for the others.
  • Preload-once designFragmentProgram compilation is a one-time async cost at app startup, not per-route.
  • Impeller-friendly — uses #version 460 core + flutter/runtime_effect.glsl, the modern Flutter shader path.

Showcase #

Diamond Circle iris Wipe
diamond circle wipe

Diamond — a grid of diamond cells. A soft band sweeps along the chosen direction; within the band each cell fills from its center outward, so the edge reads as a shimmer of growing diamonds rather than a hard line. cellSize controls the grid density.

Circle iris — a circular reveal that grows from the screen center outward, feathered at the edge for a clean anti-aliased ring. Duration is the only knob.

Wipe — a straight feathered edge that travels across the screen in any of eight directions (four axis-aligned, four diagonal). softness sets the feather width; 0 gives a hard edge.

Cover color with a 600 ms hold ("fade to black, hold, reveal new scene") — the incoming page is hidden while a flat color wipes in, holds, then wipes back out to reveal the destination:

cover-fade

Installation #

flutter pub add shader_transitions

Or in pubspec.yaml:

dependencies:
  shader_transitions: ^0.1.0

Upgrading from 0.0.x? The config API changed — see Migrating from 0.0.x.

Quick start #

1. Preload shaders at app startup. Fragment program compilation is too slow to do on the first transition.

import 'package:flutter/material.dart';
import 'package:shader_transitions/shader_transitions.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await ShaderTransitions.preload();
  runApp(const MyApp());
}

2. Push a route with one of the convenience factories:

Navigator.of(context).push(
  ShaderTransitions.diamond(
    page: const NextPage(),
    direction: SweepDirection.topLeftToBottomRight,
  ),
);

3. With go_router — wrap your page in CustomTransitionPage and pass ShaderTransitionBuilders.create. opaque: false is required so the outgoing page shows through the masked area:

GoRoute(
  path: '/next',
  pageBuilder: (context, state) => CustomTransitionPage(
    key: state.pageKey,
    child: const NextPage(),
    opaque: false, // ← required
    transitionDuration: const Duration(milliseconds: 800),
    transitionsBuilder: ShaderTransitionBuilders.create(
      const DiamondTransition(),
    ),
  ),
),

Usage #

Every transition is a sealed ShaderTransition: DiamondTransition, CircleTransition, or WipeTransition. All share duration, an optional cover, and an invert flag.

Diamond grid #

A grid of diamond cells that fan out behind a sweeping band.

Navigator.of(context).push(
  ShaderTransitions.diamond(
    page: const NextPage(),
    direction: SweepDirection.leftToRight,
    cellSize: 40.0, // diamond cell size in px (≥ 1)
    duration: const Duration(milliseconds: 800),
  ),
);

Or build the transition directly for full control:

const DiamondTransition(
  direction: SweepDirection.bottomLeftToTopRight,
  cellSize: 28.0,
  duration: Duration(milliseconds: 900),
)

Circle iris #

Circular iris reveal. origin sets where it emanates from; invert makes it contract instead of expand.

Navigator.of(context).push(
  ShaderTransitions.circle(
    page: const NextPage(),
    origin: Alignment.bottomRight,
    invert: true, // contracting iris
    duration: const Duration(milliseconds: 700),
  ),
);

Wipe #

Linear directional wipe with a feathered edge.

Navigator.of(context).push(
  ShaderTransitions.wipe(
    page: const NextPage(),
    direction: SweepDirection.rightToLeft,
    softness: 6.0, // feather width in px; 0 = hard edge
    rotation: 0.0, // radians; tilts the edge
    duration: const Duration(milliseconds: 600),
  ),
);

Cover color & hold (cinematic fade) #

Pass a TransitionCover to fill the un-revealed area with a flat color instead of letting the outgoing page show through. hold keeps the screen fully covered between the wipe-in and wipe-out.

Navigator.of(context).push(
  ShaderPageRoute(
    page: const NextPage(),
    transition: const WipeTransition(
      direction: SweepDirection.leftToRight,
      duration: Duration(milliseconds: 800),
      cover: TransitionCover(
        color: Colors.black,
        hold: Duration(milliseconds: 600), // hold on black for 600 ms
      ),
    ),
  ),
);

cover.hold is clamped internally to at most 75% of duration — each wipe always gets at least 12.5% of the timeline, so the cover hold can never squash the wipes into nothing.

Timeline with duration: 800 ms and cover.hold: 600 ms:

| cover wipes in | hold full cover     | page wipes in |
|     100 ms     |       600 ms        |    100 ms     |

Configuration reference #

Shared by every ShaderTransition:

Field Type Default
duration Duration 800 ms diamond · 700 ms circle · 600 ms wipe
cover TransitionCover? null
invert bool false

TransitionCover: color (Color, required), hold (Duration, default Duration.zero, clamped to ≤ 75% of duration).

Per-type fields:

Type Field Type Default
DiamondTransition cellSize double 40.0 (px, min 1)
feather double 0.0
direction SweepDirection topLeftToBottomRight
CircleTransition origin Alignment Alignment.center
feather double 2.0
WipeTransition softness double 4.0 (px; 0 = hard edge)
direction SweepDirection leftToRight
rotation double 0.0 (radians)

SweepDirection values: topLeftToBottomRight, topRightToBottomLeft, bottomLeftToTopRight, bottomRightToTopLeft, leftToRight, rightToLeft, topToBottom, bottomToTop.

With go_router / auto_route / app-wide #

ShaderTransitionBuilders.create(transition) returns a standard RouteTransitionsBuilder, so any router that accepts one works.

go_router:

GoRoute(
  path: '/next',
  pageBuilder: (context, state) => CustomTransitionPage(
    key: state.pageKey,
    child: const NextPage(),
    opaque: false,
    transitionDuration: const Duration(milliseconds: 800),
    transitionsBuilder: ShaderTransitionBuilders.create(
      const DiamondTransition(direction: SweepDirection.leftToRight),
    ),
  ),
),

auto_route:

AutoRoute(
  page: NextRoute.page,
  customRouteBuilder: <T>(context, child, page) => PageRouteBuilder<T>(
    settings: page,
    opaque: false,
    pageBuilder: (_, __, ___) => child,
    transitionsBuilder: ShaderTransitionBuilders.create(
      const CircleTransition(),
    ),
  ),
),

App-wide via ThemeData.pageTransitionsTheme:

MaterialApp(
  theme: ThemeData(
    pageTransitionsTheme: const PageTransitionsTheme(
      builders: {
        TargetPlatform.android: ShaderPageTransitionsBuilder(
          WipeTransition(direction: SweepDirection.leftToRight),
        ),
        TargetPlatform.iOS: ShaderPageTransitionsBuilder(CircleTransition()),
      },
    ),
  ),
)

⚠️ opaque: false is required for the outgoing page to show through the un-revealed area. ShaderPageRoute sets it for you; with go_router/auto_route you set it on the page. ShaderPageTransitionsBuilder is best paired with a cover (covered transitions don't need the route below to be visible).

Widget transitions #

ShaderTransitionSwitcher runs a transition between two widgets instead of routes — an AnimatedSwitcher analog. Key the children so swaps are detected:

ShaderTransitionSwitcher(
  transition: const CircleTransition(),
  child: KeyedSubtree(
    key: ValueKey(index),
    child: pages[index],
  ),
)

Sound on transition #

The package bundles no audio. Use the onStart / onComplete / onProgress callbacks (available on ShaderPageRoute, ShaderTransitionBuilders.create, and ShaderTransitionSwitcher) and play audio with whatever package you already use:

Navigator.of(context).push(
  ShaderPageRoute(
    page: const NextPage(),
    transition: const WipeTransition(),
    onStart: () => audioPlayer.play(AssetSource('whoosh.mp3')),
  ),
);

Migrating from 0.0.x #

0.1.0 replaced the single ShaderTransitionConfig with a sealed ShaderTransition hierarchy.

0.0.x 0.1.0
ShaderTransitionConfig.diamond(size: 40, transitionDuration: d) DiamondTransition(cellSize: 40, duration: d)
ShaderTransitionConfig.wipe(size: 6) WipeTransition(softness: 6)
ShaderTransitionConfig.circle() CircleTransition()
color: Colors.black, coverDuration: h cover: TransitionCover(color: Colors.black, hold: h)
ShaderPageRoute(config: c) ShaderPageRoute(transition: t)
ShaderTransitionBuilders.create(config) ShaderTransitionBuilders.create(transition)

The ShaderTransitions.{diamond,circle,wipe}(...) convenience factories keep working — only their parameter names changed (sizecellSize, transitionDurationduration, etc.).

How it works #

The core is ShaderMask(blendMode: BlendMode.dstIn) wrapping the incoming page — the shader writes alpha into BlendMode.dstIn, so where the shader's alpha is 0 the page is transparent and the outgoing route shows through, and where it's 1 the page is opaque.

When color != null the widget owns two FragmentShader instances and composes a three-phase Stack:

animation.value:  0 ─────── phase1End ───── phase2End ─────── 1
                  │             │              │              │
cover wipe in     │░░░░░░░░░░░░░│  full cover  │  full cover  │
                  │             │              │              │
page wipe out     │   hidden    │   hidden     │░░░░░░░░░░░░░░│
                  │             │              │              │
visible result:   outgoing →  cover     full cover     cover → new page

phase1End and phase2End are derived from transitionDuration and coverDuration. With coverDuration: Duration.zero, phases 1 and 3 abut directly — a continuous cross-fade through the color.

Used by #

  • Your project here — open a PR adding it!

If you ship a published app or open-source project that uses shader_transitions, please send a PR adding it to this list. A short description and a link is enough.

Contributing #

Issues, PRs, and discussions all welcome.

  • Bug reportsopen an issue with a minimum reproducible example and the platform you saw it on (Impeller vs Skia matters for shader behavior).
  • Feature requests → an issue with the enhancement label is fine.
  • Pull requests — fork, branch, keep commits small and focused, and make sure flutter analyze from both the repo root and example/ stays clean.
  • Editing a .frag file → run flutter clean && flutter pub get in example/ before re-launching. Flutter caches compiled shader assets and hot reload won't pick up GLSL changes on its own.

Maintainer: John Patrick Prieto

License #

MIT © 2026 John Patrick Prieto


Built by John Patrick Prieto · GitHub

1
likes
0
points
13
downloads

Publisher

verified publisherjohnpatrickprieto.com

Weekly Downloads

GPU-accelerated shader page transitions for Flutter — diamond, circle iris, and wipe with eight sweep directions, optional cover color, and a cinematic hold.

Homepage
Repository (GitHub)
View/report issues

Topics

#transitions #shaders #animation #navigation #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on shader_transitions