Drawably Flutter

pub package GitHub license

Playful, animated hand-drawn controls and decorations for Flutter. Every widget is rendered with deterministic seeded randomness and can gently “boil” between three cached sketch frames.

Animated Drawably Flutter widget showcase

Features

  • Outline, solid, and scribbled buttons
  • Checkbox, radio, and toggle controls
  • Cards, badges, and rough dividers
  • Underline, highlight, and circle decorations
  • Reproducible output using seeded randomness
  • Shared styling through DrawablyTheme
  • Keyboard navigation and semantic control states
  • Automatic reduced-motion support
  • No runtime dependency beyond Flutter

Installation

flutter pub add drawably_flutter

Then import the package:

import 'package:drawably_flutter/drawably_flutter.dart';

Quick start

DrawablyButton(
  onPressed: () {
    // Handle the press.
  },
  child: const Text('Create sketch'),
)

Buttons

Choose from three visual variants and optional state/tone values:

Wrap(
  spacing: 12,
  children: [
    DrawablyButton(
      onPressed: () {},
      child: const Text('Outline'),
    ),
    DrawablyButton(
      variant: DrawablyButtonVariant.solid,
      onPressed: () {},
      child: const Text('Solid'),
    ),
    DrawablyButton(
      variant: DrawablyButtonVariant.scribble,
      onPressed: () {},
      child: const Text('Scribble'),
    ),
  ],
)

For asynchronous actions, set state to loading, success, or error. Use tone for neutral and destructive actions.

DrawablyButton(
  state: isSaving
      ? DrawablyButtonState.loading
      : DrawablyButtonState.idle,
  tone: DrawablyTone.primary,
  onPressed: isSaving ? null : save,
  child: const Text('Save'),
)

Selection controls

Drawably controls use Flutter's familiar controlled-widget pattern. Your application owns the value and updates it in setState or your state-management solution.

DrawablyCheckbox(
  value: checked,
  semanticLabel: 'Accept terms',
  onChanged: (value) => setState(() => checked = value),
)

DrawablyRadio<int>(
  value: 1,
  groupValue: selected,
  onChanged: (value) => setState(() => selected = value),
)

DrawablyToggle(
  value: notificationsEnabled,
  semanticLabel: 'Notifications',
  onChanged: (value) {
    setState(() => notificationsEnabled = value);
  },
)

Pass null to onChanged or onPressed to render a disabled control.

Containers and decorations

Column(
  children: [
    const DrawablyCard(
      child: Text('Any Flutter widget can go here.'),
    ),
    const DrawablyDivider(),
    const Wrap(
      spacing: 16,
      children: [
        DrawablyBadge(child: Text('New')),
        DrawablyUnderline(child: Text('Important')),
        DrawablyHighlight(child: Text('Highlighted')),
        DrawablyCircle(child: Text('Look here')),
      ],
    ),
  ],
)

Styling and themes

Set a style on DrawablyTheme to configure every descendant. A style supplied directly to a widget overrides the inherited theme.

DrawablyTheme(
  data: const DrawablyStyle(
    seed: 42,
    strokeColor: Color(0xff173f5f),
    fillColor: Color(0xffffd166),
    paperColor: Color(0xfffffbf4),
    strokeWidth: 2,
    roughness: 1.1,
    boil: 0.3,
  ),
  child: const MyAppContent(),
)
Property Default Purpose
seed random Produces a reproducible sketch when provided
roughness 1.0 Controls the amount of hand-drawn wobble
boil 0.3 Controls frame-to-frame movement; use 0 for static paths
strokeColor pen blue Colors outlines and marks
fillColor white Colors solid and highlighted areas
paperColor white Colors paper-facing content in solid controls
strokeWidth 2.0 Sets line thickness

Motion and accessibility

Animated sketches cycle through three cached paths over 1.2 seconds. The paths are regenerated only when their size, seed, or visual configuration changes.

Drawably automatically freezes animation when Flutter reports that the user has requested reduced motion. Checkbox, radio, toggle, and button widgets expose semantic state and keyboard activation. Supply semanticLabel when the visible context does not already describe a control.

Low-level paths

The rough geometry functions are public, so custom CustomPainter implementations can use the same visual language:

final path = roughRoundedRect(
  RRect.fromRectAndRadius(
    const Rect.fromLTWH(0, 0, 160, 48),
    const Radius.circular(8),
  ),
  const RoughOptions(seed: 42, roughness: 1.0),
);

Also available: roughLine, roughCircle, roughEllipse, roughCheckmark, and scribbleFill.

Example

The complete showcase lives in example/lib/main.dart. Run it with:

cd example
flutter run

The package is still pre-1.0, so the public API may evolve between minor releases.

Attribution

This is an independent Flutter port inspired by Daniel Belyi's MIT-licensed Drawably JavaScript library. It is not an official upstream package.

License

MIT. The original Drawably copyright notice is preserved in LICENSE.

Libraries

drawably_flutter