pixel_ui
Pixel-art design system for Flutter β build retro, 8-bit, RPG-style game UIs with parametric shapes, tile grids, interactive buttons, pixel drop shadows, and a bundled pixel font.
π¨ Try the PixelShapeStyle tuner β

Features
- Stair-pattern asymmetric corners (
PixelCornerswith.sharp/.xs/.sm/.md/.lg/.xlpresets, plus fully custom per-corner control) - Deterministic LCG texture overlays
- Pixel-aware drop shadows with
.sm/.md/.lgfactories - Press-stateβaware interactive pixel buttons (
PixelButton) - Tile-grid layout widget (
PixelGrid<T>) for minimaps, inventories, and tile maps β with keyboard focus and drag-and-drop - Bundled Mulmaru pixel font (SIL OFL 1.1) with a ready-made
TextStylefactory - Zero external dependencies beyond the Flutter SDK
Why pixel_ui?
Most Flutter pixel/retro packages ship a complete themed widget set tied to
a specific era (NES, Windows XP, Steam). pixel_ui is positioned
differently: it provides low-level pixel primitives
(PixelShapePainter, PixelCorners, PixelShadow, PixelTexture) plus a
small set of opinionated composite widgets (PixelBox, PixelButton,
PixelGrid, PixelText) and a bundled pixel font. Compose
inventories, minimaps, dialog frames, HP bars, and tile maps from
primitives that fit your art direction β instead of inheriting someone
else's chrome.
Platforms
| Platform | Status | Verification |
|---|---|---|
| Android | β | CI build (debug APK) |
| iOS | β | CI build (no-codesign) |
| Web | β | CI build + smoke-tested on Chrome |
| macOS | β | CI build + smoke-tested locally |
| Linux | β | CI build |
| Windows | β | CI build |
Web is smoke-tested on Chrome. Other browsers (Safari, Firefox) should work but are not part of release validation. Linux and Windows are validated by CI build only β please file an issue if you find rendering glitches on those platforms.
Install
dependencies:
pixel_ui: ^0.4.0
Quick Start
import 'package:flutter/widgets.dart';
import 'package:pixel_ui/pixel_ui.dart';
final style = PixelShapeStyle(
corners: PixelCorners.lg,
fillColor: const Color(0xFF5A8A3A),
borderColor: const Color(0xFF2A4820),
borderWidth: 1,
shadow: PixelShadow.sm(const Color(0xFF1A3010)),
);
PixelButton(
logicalWidth: 60,
logicalHeight: 18,
normalStyle: style,
onPressed: () {},
child: Text(
'START',
style: PixelText.mulmaru(fontSize: 18, color: const Color(0xFFFFFFFF)),
),
);
Sizing model
logicalWidth and logicalHeight are integer pixel-art grid cells, not
screen pixels. When you don't pass width: / height:, the widget renders at
logical size Γ 4 screen pixels (so logicalWidth: 60, logicalHeight: 18
β 240Γ72 dp). Override either dimension to stretch the same logical grid to a
custom screen size β the painter still draws at the logical resolution so
pixels stay crisp. Aspect ratio is preserved if only one of width/height
is given.
The logical values also drive corner stair patterns, shadow offsets, and texture cell sizes, so think of them as the "pixel art canvas" the design is authored on.
Usage
Corners
PixelCorners describes an asymmetric per-corner stair pattern. Use the provided scale constants or compose your own:
const PixelCorners.all([3, 2, 1]) // symmetric medium
const PixelCorners.only(tl: [3, 2, 1], tr: [3, 2, 1]) // top tab
PixelCorners.sharp // all square
PixelCorners.xs // 1-pixel rounding
PixelCorners.md // 3-row stair
PixelCorners.lg // 4-row stair
Shadows
PixelShadow.sm(Colors.black) // offset (1, 1)
PixelShadow.md(Colors.black) // offset (2, 2)
PixelShadow.lg(Colors.black) // offset (4, 4)
const PixelShadow(offset: Offset(3, 2), color: Colors.black) // custom
PixelButton
PixelButton(
logicalWidth: 60,
logicalHeight: 18,
normalStyle: /* PixelShapeStyle */,
pressedStyle: /* optional, falls back to normalStyle */,
disabledStyle: /* optional, falls back to normalStyle at 50% opacity */,
pressChildOffset: const Offset(0, 1),
onPressed: () {},
semanticsLabel: 'Start',
child: /* your child widget */,
);
When onPressed is null the button is non-interactive. If you pass a
disabledStyle it renders at full opacity with that style; otherwise the
button falls back to normalStyle rendered at 50% opacity β a generic dim
that avoids a distracting visual when you don't need one.
Texture
PixelShapeStyle(
corners: PixelCorners.md,
fillColor: const Color(0xFFFFD643),
texture: const PixelTexture(
color: Color(0xFFFFF7D0),
density: 0.15,
size: 1,
seed: 7,
),
);
Textures use a deterministic LCG so identical settings produce identical patterns across platforms and builds.
Direct CustomPaint integration
For custom compositions that PixelBox doesn't cover β minimaps, tile
grids, procedural layouts β use PixelShapePainter inside your own
CustomPaint. Size the canvas with the shadow-aware helper so drop
shadows never get clipped:
import 'package:pixel_ui/pixel_ui.dart';
final style = PixelShapeStyle(
corners: PixelCorners.md,
fillColor: const Color(0xFF5A8A3A),
shadow: PixelShadow.md(const Color(0xFF1A3010)),
);
CustomPaint(
size: PixelShapePainter.canvasSizeFor(
style: style,
logicalWidth: 16,
logicalHeight: 16,
scale: 4, // default β screen pixels per logical pixel
),
painter: PixelShapePainter(
logicalWidth: 16,
logicalHeight: 16,
style: style,
),
)
The helper returns (logicalWidth Γ scale, logicalHeight Γ scale) when
there's no shadow, and expands by |shadow.offset| on each axis
otherwise. If you compute canvas size yourself, mirror the formula:
(logicalWidth + |shadow.offset.dx|) Γ scale wide by
(logicalHeight + |shadow.offset.dy|) Γ scale tall.
Tile grids
PixelGrid<T> lays out a 2D grid of PixelShapePainter tiles with
optional keyboard focus, tap callbacks, and Draggable<T>/DragTarget<T>
drag-and-drop. Use .fromList for static data or .builder for
procedural/large maps:
import 'package:pixel_ui/pixel_ui.dart';
enum Slot { sword, potion }
PixelGrid<Slot>.fromList(
data: const [
[Slot.sword, null],
[null, Slot.potion],
],
tileLogicalWidth: 10,
tileLogicalHeight: 10,
tileScreenSize: const Size(48, 48),
styleFor: (s) => s == Slot.sword ? swordStyle : potionStyle,
emptyStyle: emptySlotStyle,
dragDataFor: (x, y) => grid[y][x], // null β non-draggable tile
onTileAccept: (from, to, payload) { /* swap / merge / reject */ },
onTileActivate: (x, y) { /* arrow keys + Enter/Space or a tap */ },
autofocus: true,
)
Data indexing is data[y][x] (outer list = rows). Enter/Space activates
the focused tile only when its data is non-null β empty slots are not
"activatable".
Typography
The package bundles the Mulmaru proportional pixel font. Use the factory helper:
Text('λ¬λ €λΌ', style: PixelText.mulmaru(fontSize: 20, color: Colors.white));
Or compose a custom TextStyle using the exposed constants:
Text(
'hello',
style: TextStyle(
fontFamily: PixelText.mulmaruFontFamily,
package: PixelText.mulmaruPackage,
fontSize: 18,
),
);
Monospaced variant
For code, terminal-style UI, or fixed-width layouts, use PixelText.mulmaruMono:
Text(
'HP 042/100',
style: PixelText.mulmaruMono(fontSize: 12, color: Colors.white),
)
Gallery
| Corners | Shadows |
|---|---|
![]() |
![]() |
| Buttons | Texture |
|---|---|
![]() |
![]() |
| Tile grids | |
|---|---|
Example
See example/lib/main.dart for a full showcase of every primitive. Run:
cd example
flutter run
Bundled Font
This package bundles the Mulmaru pixel fonts (proportional + monospaced variants) by mushsooni, distributed under the SIL Open Font License 1.1.
See OFL.txt for the full font license. Apps using pixel_ui should include OFL attribution in their open-source license disclosures; Flutter's showLicensePage() handles this automatically when the bundling note (see LICENSE) is in place.
Contributing
Issues and PRs are welcome at github.com/BottlePumpkin/pixel_ui/issues.
Internal quality improvement cycles use the dogfood label. To view only user-facing issues:
open issues excluding dogfood.
License
MIT for code (see LICENSE). Bundled Mulmaru font is under SIL OFL 1.1 (see OFL.txt).
Libraries
- pixel_ui
- pixel_ui β Pixel-art design system for Flutter.



