parallax_onboarding 0.1.0 copy "parallax_onboarding: ^0.1.0" to clipboard
parallax_onboarding: ^0.1.0 copied to clipboard

A parallax onboarding carousel for Flutter with per-layer depth, slot-based pages, page-indicator dots, and first-class RTL and reduced-motion support.

example/lib/main.dart

import 'dart:async';

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

void main() => runApp(const DemoApp());

/// One onboarding slide expressed as plain data so the demo can map it onto
/// [OnboardingPage]s without repeating layout code.
class _Slide {
  const _Slide({
    required this.icon,
    required this.title,
    required this.body,
    required this.accent,
    required this.top,
    required this.bottom,
  });

  final IconData icon;
  final String title;
  final String body;
  final Color accent;
  final Color top;
  final Color bottom;
}

const List<_Slide> _slides = <_Slide>[
  _Slide(
    icon: Icons.layers_rounded,
    title: 'Depth in every swipe',
    body: 'Background, content and foreground glide at their own pace.',
    accent: Color(0xFF6C8CFF),
    top: Color(0xFF2B3A67),
    bottom: Color(0xFF0B1026),
  ),
  _Slide(
    icon: Icons.swipe_rounded,
    title: 'Swipe or tap',
    body: 'Flick through pages, or use the built-in Next and Skip controls.',
    accent: Color(0xFF36D6C3),
    top: Color(0xFF0E5A52),
    bottom: Color(0xFF05201D),
  ),
  _Slide(
    icon: Icons.accessibility_new_rounded,
    title: 'Built for everyone',
    body: 'First-class RTL and reduced-motion support, out of the box.',
    accent: Color(0xFFB388FF),
    top: Color(0xFF4A2A6B),
    bottom: Color(0xFF1A0E2B),
  ),
  _Slide(
    icon: Icons.rocket_launch_rounded,
    title: 'Ready to ship',
    body: 'Drop in your pages and onboard your users in minutes.',
    accent: Color(0xFFFFB270),
    top: Color(0xFF8A4B1F),
    bottom: Color(0xFF2B1206),
  ),
];

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'parallax_onboarding',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: Colors.black,
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(foregroundColor: Colors.white),
        ),
      ),
      home: const OnboardingDemo(),
    );
  }
}

class OnboardingDemo extends StatefulWidget {
  const OnboardingDemo({super.key});

  @override
  State<OnboardingDemo> createState() => _OnboardingDemoState();
}

class _OnboardingDemoState extends State<OnboardingDemo> {
  late final ParallaxOnboardingController _controller;
  Timer? _autoplay;
  int _direction = 1;

  @override
  void initState() {
    super.initState();
    _controller = ParallaxOnboardingController();
    // Auto-play a smooth ping-pong sweep so the README capture shows the
    // parallax in motion without anyone touching the screen.
    _autoplay = Timer.periodic(const Duration(milliseconds: 2200), (_) {
      if (!mounted) return;
      final current = _controller.index;
      if (current >= _slides.length - 1) {
        _direction = -1;
      } else if (current <= 0) {
        _direction = 1;
      }
      _controller.animateToPage(
        current + _direction,
        duration: const Duration(milliseconds: 800),
        curve: Curves.easeInOutCubic,
      );
    });
  }

  @override
  void dispose() {
    _autoplay?.cancel();
    _controller.dispose();
    super.dispose();
  }

  void _toast(String message) {
    ScaffoldMessenger.of(context)
      ..clearSnackBars()
      ..showSnackBar(
        SnackBar(
          content: Text(message),
          behavior: SnackBarBehavior.floating,
          duration: const Duration(seconds: 1),
        ),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ParallaxOnboarding(
        controller: _controller,
        onDone: () => _toast('Done!'),
        onSkip: () => _toast('Skipped'),
        background: _GradientBackground(controller: _controller),
        indicatorBuilder: (context, controller) =>
            _Dots(controller: controller),
        pages: <OnboardingPage>[
          for (final slide in _slides)
            OnboardingPage(
              backgroundFactor: -0.35,
              contentFactor: 0.0,
              foregroundFactor: 0.6,
              foregroundAlignment: Alignment.bottomRight,
              background: _Blobs(accent: slide.accent),
              content: _Content(slide: slide),
              foreground: Padding(
                padding: const EdgeInsets.only(right: 12, bottom: 96),
                child: Icon(
                  slide.icon,
                  size: 200,
                  color: slide.accent.withValues(alpha: 0.16),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

/// Cross-fades the page gradient as the controller scrolls between slides.
class _GradientBackground extends StatelessWidget {
  const _GradientBackground({required this.controller});

  final ParallaxOnboardingController controller;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, _) {
        final page = controller.page.clamp(0.0, (_slides.length - 1).toDouble());
        final lower = page.floor();
        final upper = (lower + 1).clamp(0, _slides.length - 1);
        final t = page - lower;
        final top = Color.lerp(_slides[lower].top, _slides[upper].top, t)!;
        final bottom =
            Color.lerp(_slides[lower].bottom, _slides[upper].bottom, t)!;
        return DecoratedBox(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: <Color>[top, bottom],
            ),
          ),
        );
      },
    );
  }
}

/// Two soft accent circles that drift with the foreground parallax layer.
class _Blobs extends StatelessWidget {
  const _Blobs({required this.accent});

  final Color accent;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: -60,
          left: -40,
          child: _circle(220, accent.withValues(alpha: 0.22)),
        ),
        Positioned(
          bottom: 40,
          right: -70,
          child: _circle(260, accent.withValues(alpha: 0.14)),
        ),
      ],
    );
  }

  Widget _circle(double size, Color color) => Container(
        width: size,
        height: size,
        decoration: BoxDecoration(color: color, shape: BoxShape.circle),
      );
}

/// The icon badge, title and body for a single slide.
class _Content extends StatelessWidget {
  const _Content({required this.slide});

  final _Slide slide;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            width: 64,
            height: 64,
            decoration: BoxDecoration(
              color: slide.accent.withValues(alpha: 0.18),
              borderRadius: BorderRadius.circular(18),
            ),
            child: Icon(slide.icon, size: 32, color: slide.accent),
          ),
          const SizedBox(height: 28),
          Text(
            slide.title,
            style: const TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.w700,
              color: Colors.white,
              height: 1.1,
            ),
          ),
          const SizedBox(height: 14),
          Text(
            slide.body,
            style: const TextStyle(fontSize: 16, color: Colors.white70),
          ),
        ],
      ),
    );
  }
}

/// A compact animated page indicator built on top of the controller, used to
/// show how [ParallaxOnboarding.indicatorBuilder] can be fully customized.
class _Dots extends StatelessWidget {
  const _Dots({required this.controller});

  final ParallaxOnboardingController controller;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, _) {
        final page = controller.page;
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            for (var i = 0; i < controller.count; i++)
              _dot(distance: (page - i).abs().clamp(0.0, 1.0)),
          ],
        );
      },
    );
  }

  Widget _dot({required double distance}) {
    final width = lerpDouble(22, 8, distance)!;
    final opacity = lerpDouble(1.0, 0.4, distance)!;
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      margin: const EdgeInsets.symmetric(horizontal: 3),
      width: width,
      height: 8,
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: opacity),
        borderRadius: BorderRadius.circular(4),
      ),
    );
  }
}

double? lerpDouble(num a, num b, double t) => a + (b - a) * t;
1
likes
150
points
22
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A parallax onboarding carousel for Flutter with per-layer depth, slot-based pages, page-indicator dots, and first-class RTL and reduced-motion support.

Repository (GitHub)
View/report issues

Topics

#onboarding #parallax #carousel #intro #animation

License

Apache-2.0 (license)

Dependencies

flutter, smooth_page_indicator

More

Packages that depend on parallax_onboarding