design_widgets
Native Flutter widgets for design layers: DesignRectangle, described by its
Layout, Appearance, Fill, Stroke and Effects, and DesignText, by its text,
Typography, Layout and Appearance, with the same Fill, Stroke and Effects.
They draw what Flutter's own widgets don't: smoothed corners, strokes inside, centered on or outside an edge, stacked fills, inner shadows, progressive blurs, and text placed by its font file's own measures.
Their parameters use the names and units of Figma's design panel: fills, strokes, effects, corner smoothing, line height, letter spacing, vertical trim and the rest, with colors in hex and opacities in percent. So values can be copied from Figma as they are.
Features
- Smoothed corners: a radius for each corner, and corner smoothing from 0 to 100%.
- Stacked fills: any number of solid colors and linear and radial gradients, topmost first.
- Strokes: inside, centered on or outside the edge, painted with fills of their own.
- Shadows: drop and inner shadows, with spread on shapes.
- Blurs: layer blurs, uniform or progressive from one point to another.
- Text: fonts found by the names inside their files; line height, letter spacing, alignment, auto width, auto height or fixed size, and vertical trim; the same fills, strokes and effects as shapes.
- Appearance: layer opacity, and blend modes on shapes.
- Rendering: the render objects behind the widgets, for render objects of your own that must draw the same way.
Getting started
flutter pub add design_widgets
Declare your fonts in the pubspec as usual and read them once before runApp. A layer holds no child: to put text on
a shape, lay both in a Stack.
import 'package:design_widgets/design_widgets.dart';
import 'package:flutter/widgets.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await DesignFonts.load();
runApp(const Directionality(textDirection: TextDirection.ltr, child: Center(child: GetStartedButton())));
}
class GetStartedButton extends StatelessWidget {
const GetStartedButton({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 280,
height: 72,
child: Stack(
alignment: Alignment.center,
children: [
DesignRectangle(
appearance: ShapeAppearance(cornerRadius: .all(36)),
fill: [
.linearGradient(stops: [GradientStop(0, HexColor(0x5C7CFA)), GradientStop(100, HexColor(0x3B5BDB))]),
],
stroke: Stroke(fill: [.solid(HexColor(0x1E3A8A))], weight: 3, align: .outside),
effects: [.dropShadow(position: Position(y: 4), blur: 10, color: HexColor(0x1E3A8A, opacity: 35))],
),
DesignText(
'Get started',
fontName: FontName('Roboto', 'Bold'),
fontSize: 28,
textAlignHorizontal: .center,
verticalTrim: .capHeightToBaseline,
fill: [.solid(HexColor(0xFFFFFF))],
),
],
),
);
}
}
The example app shows this button, gradients and blurs.
DesignRectangle
import 'package:design_widgets/design_widgets.dart';
const DesignRectangle(
layout: ShapeLayout(width: 320, height: 160),
appearance: ShapeAppearance(cornerRadius: .all(24), cornerSmoothing: 60),
fill: [.solid(HexColor(0xFFFFFF))],
stroke: Stroke(fill: [.solid(HexColor(0x3B5BDB))], weight: 2, align: .outside),
effects: [.dropShadow(position: Position(y: 4), blur: 12, color: HexColor(0x000000, opacity: 15))],
)
- Colors are 6-digit hex values; opacity and corner smoothing are percent; sizes are logical pixels.
- Fill, stroke and effect layers are listed topmost first.
- Without a
widthorheight, that side takes its parent's size. - It holds no child: to put content on it, lay both in a
Stack. - Like
Container, it receives taps only when it paints something, and only inside its rounded outline. - It is drawn by Flutter itself: colors, stacked fills, gradients, corner
radii, inside strokes and drop shadows behind opaque fills come out pixel
for pixel as
Container,ColoredBoxandBoxDecorationdraw them. - A see-through fill doesn't show its own drop shadow through it.
- A shadow's
bluris the width of its Gaussian at half its peak: a sigma ofblur / 2.3548. ShapeAppearance(opacity: 50)fades the whole rectangle as one, its fill, stroke and shadows together: a fill doesn't show through its stroke. With one fill and nothing else it costs no more than the fill.ShapeAppearance(blendMode: .multiply)mixes the whole rectangle, as one, with what is drawn below it: the colors below are multiplied by its own, so it only darkens..passThrough, the default, and.normaldraw it over them; they differ only in whether blend modes inside the layer reach what's below, which nothing inside a rectangle has yet.
DesignText
import 'package:design_widgets/design_widgets.dart';
const DesignText(
'Get started',
fontName: FontName('Roboto', 'Bold'),
fontSize: 22,
textAlignHorizontal: .center,
verticalTrim: .capHeightToBaseline,
fill: [.solid(HexColor(0xFFFFFF))],
stroke: Stroke(fill: [.solid(HexColor(0x1E3A8A))], weight: 2, align: .outside),
effects: [.dropShadow(position: Position(y: 2), color: HexColor(0x172554))],
)
Fonts
A font is named by its family and its style, the names inside the font file,
such as Inter and Black. Declare the files in the app's pubspec
as usual, then read them once before runApp:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await DesignFonts.load();
runApp(const App());
}
FontName('Inter', 'Black') then draws with the file named so inside,
whatever family and weight the pubspec declares it with. An app can list its
fonts as an enum:
enum AppFont implements FontName {
robotoBold('Roboto', 'Bold'),
interBlack('Inter', 'Black');
const AppFont(this.family, this.style);
@override
final String family;
@override
final String style;
}
DesignText('Get started', fontName: AppFont.robotoBold, fontSize: 22)
A font that can't be found is an error in debug builds, which names the font and draws a red box in the text's place; release builds draw it in the default font. So is a font Flutter can't pick: two files of one family declared with the same weight and style.
Typography
| Parameter | Values | Default |
|---|---|---|
fontName, fontSize |
the font, and its size in logical pixels | required |
lineHeight |
.auto, .pixels(16), .percent(120) of the size |
.auto, the font's own |
letterSpacing |
.pixels(2), .percent(5) of the size; below zero closes letters up |
.none |
textAlignHorizontal |
.left, .center, .right |
.left |
layout |
.autoWidth, .autoHeight(width: 174), .fixedSize(width: 70, height: 24, verticalAlign: .center) |
.autoWidth |
verticalTrim |
.standard, .capHeightToBaseline |
.standard |
A layout takes only the sizes it keeps fixed: auto width takes none, auto
height a width, and a fixed size both, with where the lines sit between its top
and bottom (.top, .center or .bottom).
- Auto width is the widest line rounded up to whole pixels, and a height that
follows the text is rounded to the nearest pixel. Lines break only at
\nand U+2028 in auto width, and also wrap to the width otherwise. - Line spacing is split evenly above and below each line.
- Letter spacing goes between letters, not after the last one, so a line's width and alignment are its letters'. A percent line height rounds to whole pixels, as Auto does; a line height in pixels is kept as it is.
- The size is never scaled by the system's text size setting.
- Text shadows have no spread.
appearance: TextAppearance(opacity: 50)fades the whole text as one, asShapeAppearancedoes a rectangle.- Plain text is drawn with one paragraph, as
Textdraws it; a stroke and each shadow add one more draw of the same layout.
Gradients
A gradient is placed by handles. Each handle is a GradientHandle(x, y)
given as a fraction of the box: (0, 0) is the top-left corner, (1, 1) the
bottom-right. Stops are whole percent, 0–100. opacity (percent, default
100) fades the whole gradient; each stop's color keeps its own opacity too.
// Linear: two handles, begin (0%) and end (100%).
Fill.linearGradient(
begin: GradientHandle(0, 0.5),
end: GradientHandle(1, 0.5),
stops: [GradientStop(0, HexColor(0xFE8469)), GradientStop(100, HexColor(0xF05C3A))],
)
// Radial: three handles. center (0%), end (100% along the first axis; it also
// sets the rotation) and width (100% along the second axis).
Fill.radialGradient(
center: GradientHandle(0, 0),
end: GradientHandle(1, 0),
width: GradientHandle(0, 1),
stops: [GradientStop(0, HexColor(0xFE8469)), GradientStop(100, HexColor(0xF05C3A))],
)
Handles from an SVG gradient
Divide the numbers below by the shape's width (x) and height (y).
Linear: <linearGradient x1="…" y1="…" x2="…" y2="…">
begin= (x1,y1),end= (x2,y2)
Radial: <radialGradient … gradientTransform="translate(cx cy) rotate(a) scale(rx ry)">
(a single scale(r) means rx = ry = r)
center= (cx,cy)end= (cx + rx·cos a,cy + rx·sin a)width= (cx − ry·sin a,cy + ry·cos a)
With gradientTransform="matrix(a b c d e f)" instead:
center= (e,f),end= (e + a,f + b),width= (e + c,f + d)
For example, on a 300×100 shape, translate(0 0) rotate(0) scale(300 100)
gives center (0, 0), end (300, 0) → (1, 0) and width (0, 100) →
(0, 1).
Layer blur
A layer blur blurs the whole layer, its fill, stroke and shadows together. It goes in effects with the shadows, on
a rectangle or a text:
// The same everywhere.
effects: [.layerBlur(blur: 4)]
// Progressive: sharp at the top edge's middle, a blur of 8 at the bottom edge's.
effects: [.progressiveBlur(endBlur: 8)]
// From a blur of 2 at the left edge's middle to 12 at the right edge's.
effects: [.progressiveBlur(start: GradientHandle(0, 0.5), end: GradientHandle(1, 0.5), startBlur: 2, endBlur: 12)]
- A blur is measured as a shadow's: the width of its Gaussian at half its peak, a sigma of
blur / 2.3548. It reaches as far past the layer as the blur. - A progressive blur's handles are placed as a linear gradient's: fractions of the box. Between them the blur changes in proportion, and past either one it keeps that handle's blur.
- A progressive blur is drawn as a few uniform blurs, their sigmas at most half a pixel apart and at most 16, each weighed along the line; each is one more layer drawn.
Rendering
package:design_widgets/rendering.dart holds what the widgets draw with: RenderDesignRectangle,
RenderDesignText, the text's TextPaintPlan, the corner outline, and the paints of fills and shadows. A render
object of an app's own, such as one drawing letters from outlines, uses them to draw exactly as the widgets do.
Tests
flutter test runs the widgets' tests, including pixel-for-pixel comparisons
with Container, ColoredBox, BoxDecoration and RichText. The text tests
use Roboto, whose licence is in test/fonts.
License
Apache License 2.0: see LICENSE and NOTICE. As its section 6 says, it grants no right to the
name design_widgets or its author's name.
Figma is a trademark of Figma, Inc. design_widgets is not affiliated with, sponsored or endorsed by Figma, Inc.
Libraries
- design_widgets
- Widgets that draw design layers (DesignRectangle, DesignText);
rendering.dartexposes their render objects. - rendering
- The render objects behind the design widgets, for an app's own render objects that must draw as they do.