ashlarui

Skeleton

A pulsing placeholder in the shape of content still loading.

Skeleton, as it looks in an app Skeleton, as it looks in an app, dark
import 'package:flutter/widgets.dart';

import 'ui/skeleton.dart';

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

  @override
  Widget build(BuildContext context) => const Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Skeleton(width: 48, height: 48, circle: true),
      SizedBox(width: 16),
      Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [Skeleton(width: 220), SizedBox(height: 8), Skeleton(width: 160)],
      ),
    ],
  );
}

Try its options

Each option its constructor takes, to change and see; the code below it is what you chose, to copy.

Skeleton with controls, as it looks in an app Skeleton with controls, as it looks in an app, dark

Add it

In your Flutter project:

$ ashlar add skeleton

It writes theme.dart, palette.dart and skeleton.dart into lib/ui, keeping any you already have.

Examples

Other ways to use it, each running live, with its code.

A card loading

Grey shapes where the content will be.

A card loading, as it looks in an app A card loading, as it looks in an app, dark
// A card loading: Grey shapes where the content will be.
import 'package:flutter/widgets.dart';

import 'ui/skeleton.dart';

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

  @override
  Widget build(BuildContext context) => const SizedBox(
    width: 320,
    child: Row(
      children: [
        Skeleton(width: 48, height: 48, circle: true),
        SizedBox(width: 12),
        Expanded(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [Skeleton(width: 180), SizedBox(height: 8), Skeleton(width: 120)],
          ),
        ),
      ],
    ),
  );
}

API

Read from the source above: each parameter with its type, its default, and what it is for.

Skeletonclass

A placeholder in the shape of content that is still loading. It pulses, unless the device asks for less motion.

Parameter Type Default What it is for
width double? Null fills the width it is given.
heightdouble16Its height.
circle bool false Round, for where an avatar will be.

Source

What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add will not write over it.

lib/ui/skeleton.dart 71 lines
// Ashlar UI: Skeleton. Copied into your app by `ashlar add skeleton`; it is yours to change.

import 'package:flutter/widgets.dart';

import 'theme.dart';

/// A placeholder in the shape of content that is still loading. It pulses, unless the
/// device asks for less motion.
///
/// ```dart
/// Row(children: [Skeleton(width: 40, height: 40, circle: true), SizedBox(width: 12), Skeleton(width: 160, height: 16)])
/// ```
class Skeleton extends StatefulWidget {
  const Skeleton({
    super.key,
    this.width,
    this.height = 16,
    this.circle = false,
  });

  /// Null fills the width it is given.
  final double? width;

  /// Its height.
  final double height;

  /// Round, for where an avatar will be.
  final bool circle;

  @override
  State<Skeleton> createState() => _SkeletonState();
}

class _SkeletonState extends State<Skeleton>
    with SingleTickerProviderStateMixin {
  late final _pulse = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 1),
  )..repeat(reverse: true);

  @override
  void dispose() {
    _pulse.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final t = AshlarTheme.of(context);
    final still = MediaQuery.maybeDisableAnimationsOf(context) ?? false;
    final box = Container(
      width: widget.width,
      height: widget.height,
      decoration: BoxDecoration(
        color: t.colors.accent,
        shape: widget.circle ? BoxShape.circle : BoxShape.rectangle,
        borderRadius: widget.circle ? null : BorderRadius.circular(t.radiusMd),
      ),
    );
    return ExcludeSemantics(
      child: still
          ? box
          : FadeTransition(
              opacity: Tween(begin: 1.0, end: .5).animate(
                CurvedAnimation(parent: _pulse, curve: Curves.easeInOut),
              ),
              child: box,
            ),
    );
  }
}