Vize
A modern, developer-friendly Flutter package for effortless responsive UIs that match your Figma designs perfectly.
Percentage-based sizing, Figma-direct scaling, adaptive grids, breakpoint overrides, handy extensions, and responsive builders that is smooth across mobile, tablet, and desktop. No more responsiveness headaches.
Features
- Percentage-Based Layouts: intuitive width/height sizing
- Figma Scaling: direct scaling from your design artboard
- Device Detection: automatic mobile, tablet, desktop classification
- Orientation Support: portrait and landscape handling
- Adaptive Helpers
adaptiveColumns,adaptiveValue<T>, device flags - Elegant Syntax: number extensions for clean, readable code
- User Font Scaling: app-wide text size preference via
textScalar - Lightweight: zero dependencies outside Flutter
Installation
dependencies:
vize: ^1.0.4
flutter pub get
Quick Start
1. Initialize in MaterialApp.builder
Always initialize inside MaterialApp.builder so Vize re-initializes on every rebuild, picking up orientation changes, window resizes, and updated preferences:
import 'package:flutter/material.dart';
import 'package:vize/vize.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
Vize.init(
context,
figmaWidth: 390, // your Figma artboard width
figmaHeight: 844, // your Figma artboard height
);
return child!;
},
home: const HomePage(),
);
}
}
Do not call
Vize.initin a plainbuildmethod aboveMaterialApp. TheMediaQueryis not available there, and the config will not re-apply on orientation changes. Always useMaterialApp.builder.
2. Use helpers or extensions
// Helpers
Container(
width: w(50), // 50% of screen width
height: h(30), // 30% of screen height
padding: pa(16), // scaled padding
child: Text('Hello', style: TextStyle(fontSize: ts(18))),
)
// Extensions have identical result, cleaner syntax
Container(
width: 50.w,
height: 30.h,
padding: 16.pa,
child: Text('Hello', style: TextStyle(fontSize: 18.ts)),
)
Scaling Approaches
Vize offers two complementary scaling strategies. Use them together for best results.
Percentage-based for flexible containers
Container(width: w(100), height: h(25)) // full width, quarter height
hs(2) // 2% height spacer
ws(5) // 5% width spacer
Figma-based for pixel-perfect components
Takes a value from your Figma design and scales it proportionally to the current screen.
Container(
width: 200.fw, // or fw(200)
height: 100.fh, // or fh(100)
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r), // scaled radius
),
)
Responsive Padding
pa(16) // all sides
ps(h: 20, v: 10) // symmetric horizontal / vertical
po(l: 16, t: 8) // individual sides (l, t, r, b)
// Extensions
16.pa
Spacing
hs(2) // 2% height gap
ws(5) // 5% width gap
sp() // one 8px-grid step, Figma-scaled ~8dp on standard canvas
sp(2) // two steps ~16dp
sp(3) // three steps ~24dp
sp()scales relative to your Figma canvas width viasw(). On devices wider than your canvas the value grows proportionally, which is intentional.
Device Detection
isMobile // true if width < 600
isTablet // true if 600 <= width < 1024
isDesktop // true if width >= 1024
// In a build method
if (isMobile) return const MobileLayout();
Or use VizeBuilder for declarative switching:
VizeBuilder(
mobile: (ctx) => const MobileLayout(),
tablet: (ctx) => const TabletLayout(),
desktop: (ctx) => const DesktopLayout(),
)
Adaptive Helpers
// Adaptive column count for grids
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: adaptiveColumns(mobile: 2, tablet: 4, desktop: 6),
),
...
)
// Adaptive value typed, returns different values per device
final fontSize = adaptiveValue<double>(mobile: 14, tablet: 16, desktop: 18);
final columns = adaptiveValue<int>(mobile: 1, tablet: 2, desktop: 3);
User Font Scaling
Pass a textScalar multiplier to honour a user's font-size preference. Every ts() call across the app scales accordingly and no extra MediaQuery wrapper needed.
Vize.init(
context,
textScalar: userFontScale, // e.g. 0.85 / 1.0 / 1.15 with default: 1.0
);
VizeLayout's local reactive info
Use VizeLayout when a widget needs to respond to its own constraints (not the full screen), such as inside a Column or a card:
VizeLayout(
builder: (context, info) {
return Column(
children: [
Text('Device: ${info.device}'),
Text('Orientation: ${info.orientation}'),
Text('Portrait: ${info.isPortrait}'),
Text('Screen: ${info.vizeScreen}'),
Text('Widget: ${info.vizeWidget}'),
],
);
},
)
VizeLayout now uses Vize.getInfo internally and never modifies the global Vize.I singleton, so your figmaWidth, figmaHeight, textScalar, and breakpoints set in MaterialApp.builder remain intact.
Note:
VizeLayoutusesLayoutBuilderinternally and requires bounded height constraints. If placing it inside aColumnorRow, wrap it in aSizedBoxorExpandedto avoid an unbounded constraints error.
Custom Breakpoints
Vize.init(
context,
breakpoints: const VizeBreakpoints(mobile: 600, tablet: 1024),
);
| Device | Default range |
|---|---|
| Mobile | < 600px |
| Tablet | 600 - 1024px |
| Desktop | >= 1024px |
API Reference
Vize.init parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
context |
BuildContext |
required | Source of MediaQuery dimensions |
figmaWidth |
double? |
390 |
Figma artboard width |
figmaHeight |
double? |
844 |
Figma artboard height |
breakpoints |
VizeBreakpoints? |
VizeBreakpoints() |
Mobile / tablet / desktop width thresholds |
textScalar |
double |
1.0 |
Multiplier applied to all ts() return values |
Helper functions
| Helper | Returns | Description |
|---|---|---|
w(percent) |
double |
% of screen width |
h(percent) |
double |
% of screen height |
fw(value) |
double |
Figma-scaled width |
fh(value) |
double |
Figma-scaled height |
ts(size) |
double |
Responsive text size × textScalar |
r(value) |
double |
Scaled border radius |
pa(value) |
EdgeInsets |
Scaled padding, all sides |
ps({h, v}) |
EdgeInsets |
Scaled symmetric padding |
po({l,t,r,b}) |
EdgeInsets |
Scaled individual-side padding |
ws(percent) |
SizedBox |
Width spacer (% of screen) |
hs(percent) |
SizedBox |
Height spacer (% of screen) |
fws(value) |
SizedBox |
Width spacer (Figma-scaled) |
fhs(value) |
SizedBox |
Height spacer (Figma-scaled) |
sp([step]) |
double |
8px-grid step, Figma-scaled |
adaptiveColumns |
int |
Column count by device |
adaptiveValue<T> |
T |
Typed value by device |
Number extensions
50.w // % screen width 50.h // % screen height
100.fw // Figma-scaled width 50.fh // Figma-scaled height
18.ts // text size 12.r // border radius
16.pa // EdgeInsets.all 5.ws // width SizedBox
2.hs // height SizedBox 100.fws // Figma width SizedBox
50.fhs // Figma height SizedBox
Widgets
| Widget | Description |
|---|---|
VizeBuilder |
Declarative device-switching builder (reads global Vize.I only) |
VizeLayout |
LayoutBuilder wrapper that provides local VizeInfo |
VizeScope |
InheritedWidget that exposes VizeInfo to its subtree |
VizeWrapper |
Convenience alias for VizeScope (backward-compatible) |
VizeScope
VizeScope is an InheritedWidget inserted automatically by VizeLayout. It lets any descendant read the nearest VizeInfo without prop-drilling:
// Reading info anywhere below a VizeLayout (or a manual VizeScope)
final info = VizeScope.of(context); // throws if no ancestor found
final info = VizeScope.maybeOf(context); // returns null if not found
if (info.isPortrait) { ... }
You can also place a VizeScope manually around a subtree:
VizeScope(
info: Vize.I.info,
child: MySubtree(),
)
VizeBuildernote:VizeBuilderreads the globalVize.Isingleton directly and does not respond to aVizeScopeancestor override. UseVizeLayoutwhen you need scope-aware device info.
VizeInfo properties
| Property | Type | Description |
|---|---|---|
device |
VizeDevice |
mobile / tablet / desktop |
orientation |
Orientation |
portrait or landscape |
isPortrait |
bool |
Orientation check |
isLandscape |
bool |
Orientation check |
isMobile |
bool |
Device check |
isTablet |
bool |
Device check |
isDesktop |
bool |
Device check |
vizeScreen |
Size |
Full screen size |
vizeScreenSize |
Size |
Alias for vizeScreen |
vizeWidget |
Size |
Local widget constraints size |
vizeWidgetSize |
Size |
Alias for vizeWidget |
Complete Example
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
Vize.init(context, figmaWidth: 390, figmaHeight: 844);
return child!;
},
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
padding: 20.pa,
child: Column(
children: [
Container(
width: 100.w,
height: 20.h,
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: Text('Header', style: TextStyle(fontSize: 22.ts)),
),
),
2.hs,
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: adaptiveColumns(mobile: 1, tablet: 2, desktop: 4),
mainAxisSpacing: sp(2),
crossAxisSpacing: sp(2),
),
itemBuilder: (context, i) => Container(
color: Colors.grey[200],
alignment: Alignment.center,
child: Text('Item $i'),
),
),
],
),
),
);
}
}
Best Practices
- Always init in
MaterialApp.builderand never in abuildmethod aboveMaterialApp. - Percentages for layout, Figma values for components as percentages flex naturally; Figma values replicate your design exactly.
- Wire
textScalarfrom a preference provider if your app supports user font-size preferences. BecauseMaterialApp.builderrebuilds on every change, the scalar stays live automatically. Omitting it is also valid since it defaults to1.0. - Use
VizeLayoutfor widget-local responsiveness and it won't disrupt your global config. - Test on multiple form factors like small phones, tablets, and desktop windows.
- Reset
Vizestate between widget tests by callingVize.initinsetUp. There is noVize.reset()so re-initialising is the intended pattern:
setUp(() {
// Provide a minimal fake context or use a testable wrapper
Vize.init(context, figmaWidth: 390, figmaHeight: 844);
});
Contributing
Contributions are welcome with new features, bug fixes, or docs improvements.
- Fork the repo and create a feature branch:
git checkout -b my-feature - Run the pre-flight checks before opening a PR:
- Windows:
./check.ps1 - macOS/Linux:
chmod +x check.sh && ./check.sh
- Windows:
- Open a Pull Request on GitHub.
Please follow the existing code style and include tests where appropriate.
License
MIT - see LICENSE for details.
Support
If Vize helps you ship faster, a ⭐ on GitHub goes a long way!
Issues: GitHub Issues
Made with ❤️ for the Flutter community.