Vroxal Design — Flutter
A Flutter implementation of the Vroxal Design System, providing a complete set of design tokens, a 1,598-glyph icon library, and production-ready UI components that automatically adapt to light and dark mode.
Table of Contents
Setup
1 · Add the dependency
# pubspec.yaml
dependencies:
vroxaldesign:
path: ../vd-flutter # or your pub.dev / git reference
2 · Declare the Poppins font
The design system uses Poppins. Add the font files to your app's pubspec.yaml:
flutter:
fonts:
- family: Poppins
fonts:
- asset: fonts/Poppins-Regular.ttf
- asset: fonts/Poppins-Medium.ttf
weight: 500
- asset: fonts/Poppins-SemiBold.ttf
weight: 600
- asset: fonts/Poppins-Italic.ttf
style: italic
The icon font (
VroxalIcon) is bundled inside the package — no extra declaration needed in your app.
3 · Apply the theme
Wrap your app with MaterialApp and set brightness so VdColorScheme.of(context) resolves correctly:
import 'package:flutter/material.dart';
MaterialApp(
theme: ThemeData(brightness: Brightness.light),
darkTheme: ThemeData(brightness: Brightness.dark),
themeMode: ThemeMode.system,
home: const MyHomePage(),
);
4 · Import the package
import 'package:vroxaldesign/vroxaldesign.dart';
Design Tokens
All tokens live in lib/src/tokens/ and are re-exported from the top-level barrel.
Colors
The color system uses a three-layer architecture:
| Layer | Class | Visibility |
|---|---|---|
| 1 · Raw palette | _VdPalette |
private |
| 2 · Brand aliases | _VdBrand |
private |
| 3 · Semantic scheme | VdColorScheme |
public |
Always consume VdColorScheme — never raw palette values.
final scheme = VdColorScheme.of(context); // auto light/dark
Container(color: scheme.backgroundDefaultBase)
Text('Hello', style: TextStyle(color: scheme.contentDefaultBase))
Factory constructors
VdColorScheme.light() // explicit light
VdColorScheme.dark() // explicit dark
VdColorScheme.of(ctx) // resolves from Theme.of(ctx).brightness
Semantic token groups (each available in light + dark):
| Group | Example tokens |
|---|---|
backgroundDefault* |
Base · Secondary · Tertiary · Disabled |
backgroundPrimary* |
Base · Secondary · Tertiary |
backgroundNeutral* |
Base · Secondary · Tertiary |
backgroundSuccess* |
Base · Secondary · Tertiary |
backgroundError* |
Base · Secondary · Tertiary |
backgroundWarning* |
Base · Secondary · Tertiary |
backgroundInfo* |
Base · Secondary · Tertiary |
contentDefault* |
Base · Secondary · Tertiary · Disabled |
contentPrimary* |
Base · OnBase · OnSecondary |
contentNeutral* |
Base · OnBase · OnSecondary |
contentSuccess/Error/Warning/Info* |
Base · OnBase · OnSecondary |
borderDefault* |
Base · Secondary · Tertiary · Disabled |
borderPrimary/Neutral/Success/Error/Warning/Info* |
Base · Secondary · Tertiary |
Typography
Font: Poppins — weights 400, 500, 600 + italic.
Text('Title', style: VdFont.titleLarge.textStyle)
Text('Body', style: VdFont.bodyMedium.textStyle.copyWith(color: scheme.contentDefaultBase))
Available styles (via VdFont):
| Token | Size | Weight |
|---|---|---|
displayLarge |
57 pt | SemiBold 600 |
displayMedium |
45 pt | SemiBold 600 |
displaySmall |
36 pt | SemiBold 600 |
headlineLarge |
32 pt | SemiBold 600 |
headlineMedium |
28 pt | SemiBold 600 |
headlineSmall |
24 pt | SemiBold 600 |
titleLarge |
22 pt | SemiBold 600 |
titleMedium |
16 pt | SemiBold 600 |
titleSmall |
14 pt | SemiBold 600 |
labelLarge |
14 pt | Medium 500 |
labelMedium |
12 pt | Medium 500 |
labelSmall |
11 pt | Medium 500 |
labelExtraSmall |
10 pt | Medium 500 |
bodyExtraLarge |
18 pt | Regular 400 |
bodyLarge |
16 pt | Regular 400 |
bodyMedium |
14 pt | Regular 400 |
bodyMediumItalic |
14 pt | Regular 400, italic |
bodySmall |
12 pt | Regular 400 |
bodyExtraSmall |
10 pt | Regular 400 |
Spacing & Scale
Padding(padding: EdgeInsets.all(VdSpacing.s400)) // 16 pt
SizedBox(height: VdSpacing.s200) // 8 pt
| Token | Value |
|---|---|
s0 |
0 pt |
s50 |
2 pt |
s100 |
4 pt |
s200 |
8 pt |
s300 |
12 pt |
s400 |
16 pt |
s600 |
24 pt |
s800 |
32 pt |
s1000 |
40 pt |
s1200 |
48 pt |
s1600 |
64 pt |
s1800 |
80 pt |
s2400 |
96 pt |
s3000 |
120 pt |
neg50 … neg600 |
negative steps |
Radius
BorderRadius.circular(VdRadius.md) // 12 pt
| Token | Value |
|---|---|
none |
0 pt |
xs |
4 pt |
sm |
8 pt |
md |
12 pt |
lg |
16 pt |
xl |
24 pt |
xxl |
32 pt |
xxxl |
40 pt |
full |
120 pt (pill / circle) |
Border Width
Border.all(width: VdBorderWidth.sm) // 1 pt
| Token | Value |
|---|---|
none |
0 pt |
sm |
1 pt — default border |
md |
2 pt — focus ring |
lg |
4 pt |
xl |
8 pt |
Icon Size
VdIcon(VdIcons.star, size: VdIconSize.md) // 24 pt
| Token | Value |
|---|---|
xs |
16 pt |
sm |
20 pt |
md |
24 pt ← default |
lg |
32 pt |
xl |
40 pt |
Icons
VdIcon widget
VdIcon renders a glyph from the bundled VroxalIcon font (or any IconData). It respects the nearest IconTheme for size and color fallbacks.
VdIcon(VdIcons.check_circle_filled)
VdIcon(VdIcons.xmark, size: VdIconSize.sm, color: scheme.contentErrorBase)
VdIcon(VdIcons.magnifier, semanticLabel: 'Search')
| Parameter | Type | Default |
|---|---|---|
icon |
IconData |
required |
size |
double? |
IconTheme size → VdIconSize.md |
color |
Color? |
IconTheme color |
semanticLabel |
String? |
— |
Using inside a component with IconTheme propagation:
IconTheme.merge(
data: IconThemeData(color: scheme.contentDefaultSecondary, size: VdIconSize.md),
child: myIconWidget, // Widget? — VdIcon or Icon() both work
)
VdIcons catalog
VdIcons exposes 1,598 icon constants generated from the Vroxal SVG library via fluttericon.com.
import 'package:vroxaldesign/vroxaldesign.dart';
VdIcons.check
VdIcons.xmark
VdIcons.chevron_down
VdIcons.chevron_up
VdIcons.chevron_left
VdIcons.chevron_right
VdIcons.info_circle_filled
VdIcons.check_circle_filled
VdIcons.danger_circle_filled
VdIcons.danger_triangle_filled
VdIcons.magnifier
VdIcons.minus
VdIcons.star
// … 1,585 more
All constants are static const IconData values using font family VroxalIcon with codepoints starting at 0xe800.
Components
All components are exported from package:vroxaldesign/vroxaldesign.dart.
Actions
VdButton
A full-featured button with multiple color variants and sizes.
VdButton('Save', onPressed: () {})
VdButton('Delete',
color: VdButtonColor.error,
size: VdButtonSize.small,
onPressed: _handleDelete,
)
VdButton('Upload',
leadingIcon: VdIcon(VdIcons.upload),
isLoading: _uploading,
onPressed: _upload,
)
| Parameter | Type | Default |
|---|---|---|
| (positional) label | String |
required |
color |
VdButtonColor |
.primary |
size |
VdButtonSize |
.medium |
style |
VdButtonStyle |
.filled |
leadingIcon |
Widget? |
— |
trailingIcon |
Widget? |
— |
isLoading |
bool |
false |
isDisabled |
bool |
false |
onPressed |
VoidCallback? |
— |
Enums:
VdButtonColor—primary · neutral · success · error · warning · infoVdButtonSize—large · medium · smallVdButtonStyle—filled · outlined · transparent
VdIconButton
A square icon-only button.
VdIconButton(
icon: VdIcon(VdIcons.xmark),
onPressed: _close,
)
VdIconButton(
icon: VdIcon(VdIcons.trash),
color: VdIconButtonColor.error,
style: VdIconButtonStyle.outlined,
size: VdIconButtonSize.small,
onPressed: _delete,
)
| Parameter | Type | Default |
|---|---|---|
icon |
Widget |
required |
color |
VdIconButtonColor |
.primary |
size |
VdIconButtonSize |
.medium |
style |
VdIconButtonStyle |
.filled |
isDisabled |
bool |
false |
onPressed |
VoidCallback? |
— |
VdChip
An inline tag / pill with an optional dismiss action.
VdChip('Design')
VdChip('Flutter', color: VdChipColor.primary, onRemove: _removeTag)
VdChip('Active', size: VdChipSize.small, leadingIcon: VdIcon(VdIcons.check))
| Parameter | Type | Default |
|---|---|---|
| (positional) label | String |
required |
color |
VdChipColor |
.neutral |
size |
VdChipSize |
.medium |
leadingIcon |
Widget? |
— |
onRemove |
VoidCallback? |
— |
Displays
VdIcon / VdIcons
See the Icons section above.
VdBadge
A numeric or dot indicator for navigation items or avatars.
VdBadge(count: 3)
VdBadge(count: 99, maxCount: 99)
VdBadge.dot() // dot variant, no count
| Parameter | Type | Default |
|---|---|---|
count |
int? |
— |
maxCount |
int |
99 |
dot |
bool |
false |
color |
VdBadgeColor |
.error |
VdDivider
A horizontal rule using semantic border tokens.
VdDivider()
VdDivider(color: VdDividerColor.strong)
| Parameter | Type | Default |
|---|---|---|
color |
VdDividerColor |
.defaultColor |
Feedbacks
VdAlert
An inline contextual message banner.
VdAlert(description: 'Your changes have been saved.')
VdAlert(
color: VdAlertColor.error,
title: 'Upload failed',
description: 'Please check your connection and try again.',
closable: true,
onClose: () {},
)
VdAlert(
color: VdAlertColor.warning,
description: 'This action cannot be undone.',
action: 'Learn more',
actionInline: true,
onAction: _openDocs,
)
| Parameter | Type | Default |
|---|---|---|
color |
VdAlertColor |
.primary |
icon |
Widget? |
color-mapped icon |
title |
String? |
— |
description |
String |
required |
action |
String? |
— |
actionInline |
bool |
false |
closable |
bool |
false |
onAction |
VoidCallback? |
— |
onClose |
VoidCallback? |
— |
VdAlertColor — primary · neutral · success · error · warning · info
VdEmptyState
A full zero-state layout with icon, title, description, and optional actions.
VdEmptyState(
title: 'Nothing here yet',
description: 'Add your first item to get started.',
)
VdEmptyState(
boxed: true,
icon: VdIcon(VdIcons.folder_open, size: VdIconSize.xl),
title: 'No documents',
description: 'Upload a file or create a new document.',
primaryActionTitle: 'New document',
secondaryActionTitle: 'Upload',
onPrimaryAction: _createDoc,
onSecondaryAction: _upload,
)
| Parameter | Type | Default |
|---|---|---|
title |
String |
'Title goes here' |
description |
String |
placeholder text |
icon |
Widget? |
VdIcons.info_circle_filled |
boxed |
bool |
true |
primaryActionTitle |
String? |
— |
secondaryActionTitle |
String? |
— |
onPrimaryAction |
VoidCallback? |
— |
onSecondaryAction |
VoidCallback? |
— |
Max content width: 640 pt. Max text block width: 480 pt.
VdLoadingState
A centered loading spinner with optional title and description.
VdLoadingState()
VdLoadingState(title: 'Loading…', description: 'Please wait while we fetch your data.')
VdLoadingState(boxed: false)
| Parameter | Type | Default |
|---|---|---|
title |
String? |
— |
description |
String? |
— |
boxed |
bool |
true |
VdSnackbar
A transient message bar shown at the bottom of the screen.
// Show programmatically:
VdSnackbarController.show(
context,
VdSnackbar(
message: 'Item deleted',
color: VdSnackbarColor.neutral,
action: 'Undo',
onAction: _undo,
),
);
| Parameter | Type | Default |
|---|---|---|
message |
String |
required |
color |
VdSnackbarColor |
.neutral |
action |
String? |
— |
closable |
bool |
false |
onAction |
VoidCallback? |
— |
onClose |
VoidCallback? |
— |
VdSnackbarColor — primary · neutral · success · error · warning · info
Forms
All form inputs share a common set of states via VdInputState:
enum VdInputState { normal, focus, disabled, error, success, warning }
Every input shows an optional label, helperText, isOptional badge, and a color-coded status icon driven by the active state.
VdTextField
A single-line text input.
final _ctrl = TextEditingController();
VdTextField(
controller: _ctrl,
label: 'Email',
placeholder: 'you@example.com',
helperText: 'We will never share your email.',
leadingIcon: VdIcon(VdIcons.mail),
)
VdTextField(
controller: _ctrl,
label: 'Password',
isPassword: true,
state: VdInputState.error,
helperText: 'Must be at least 8 characters.',
)
| Parameter | Type | Default |
|---|---|---|
controller |
TextEditingController |
required |
label |
String? |
— |
placeholder |
String |
'Placeholder' |
helperText |
String? |
— |
isOptional |
bool |
false |
isPassword |
bool |
false |
leadingIcon |
Widget? |
— |
trailingIcon |
Widget? |
— |
onTrailingAction |
VoidCallback? |
— |
characterLimit |
int? |
— |
state |
VdInputState |
.normal |
onChanged |
ValueChanged<String>? |
— |
VdTextArea
A multi-line text input (min 4 lines, max 9 lines then scrollable).
final _notes = TextEditingController();
VdTextArea(
controller: _notes,
label: 'Notes',
placeholder: 'Enter your notes…',
characterLimit: 500,
)
Same parameters as VdTextField except isPassword is not available.
VdSelectField
A tap-to-open dropdown backed by a bottom-sheet picker.
String? _country;
VdSelectField<String>(
label: 'Country',
selection: _country,
options: ['Australia', 'Canada', 'New Zealand', 'United States'],
onChanged: (v) => setState(() => _country = v),
)
// Custom display label:
VdSelectField<MyEnum>(
selection: _pick,
options: MyEnum.values,
optionLabel: (e) => e.displayName,
onChanged: (v) => setState(() => _pick = v),
)
| Parameter | Type | Default |
|---|---|---|
selection |
T? |
required |
options |
List<T> |
required |
optionLabel |
String Function(T)? |
toString() |
label |
String? |
— |
placeholder |
String |
'Select an option' |
helperText |
String? |
— |
isOptional |
bool |
false |
leadingIcon |
Widget? |
— |
state |
VdInputState |
.normal |
onChanged |
ValueChanged<T?>? |
— |
VdDateTimeField
A tap-to-open date/time picker field.
DateTime? _start;
VdDateTimeField(
'Start Date',
selection: _start,
mode: VdDateTimeFieldMode.date,
onChanged: (d) => setState(() => _start = d),
)
VdDateTimeField(
'Meeting Time',
selection: _time,
mode: VdDateTimeFieldMode.time,
onChanged: (d) => setState(() => _time = d),
)
| Parameter | Type | Default |
|---|---|---|
| (positional) label | String |
required |
selection |
DateTime? |
required |
mode |
VdDateTimeFieldMode |
.dateTime |
placeholder |
String |
'Placeholder' |
state |
VdInputState |
.normal |
isOptional |
bool |
false |
minimumDate |
DateTime? |
DateTime(1900) |
maximumDate |
DateTime? |
DateTime(2100) |
leadingIcon |
Widget? |
— |
trailingIcon |
Widget? |
— |
onTrailingAction |
VoidCallback? |
— |
helperText |
String? |
— |
onChanged |
ValueChanged<DateTime?>? |
— |
VdDateTimeFieldMode — date · time · dateTime
VdSearchField
A search bar with a built-in search icon and a clear button.
final _search = TextEditingController();
VdSearchField(controller: _search, placeholder: 'Search products…')
| Parameter | Type | Default |
|---|---|---|
controller |
TextEditingController |
required |
placeholder |
String |
'Search' |
state |
VdInputState |
.normal |
onChanged |
ValueChanged<String>? |
— |
VdCheckbox
A labeled checkbox with tri-state support (checked · unchecked · indeterminate).
VdCheckbox(
label: 'Accept terms',
value: _accepted,
onChanged: (v) => setState(() => _accepted = v ?? false),
)
// Indeterminate (select-all parent):
VdCheckbox(
label: 'Select all',
value: null, // null = indeterminate
tristate: true,
onChanged: _toggleAll,
)
| Parameter | Type | Default |
|---|---|---|
label |
String |
required |
value |
bool? |
required |
tristate |
bool |
false |
state |
VdInputState |
.normal |
onChanged |
ValueChanged<bool?>? |
— |
VdRadioButton / VdRadioGroup
Radio options scoped to a shared group value.
String? _plan;
VdRadioGroup<String>(
value: _plan,
onChanged: (v) => setState(() => _plan = v),
child: Column(
children: [
VdRadioOption(value: 'free', label: 'Free'),
VdRadioOption(value: 'pro', label: 'Pro'),
VdRadioOption(value: 'team', label: 'Team'),
],
),
)
VdRadioGroup |
Type | Default |
|---|---|---|
value |
T? |
required |
onChanged |
ValueChanged<T?>? |
— |
child |
Widget |
required |
VdRadioOption |
Type | Default |
|---|---|---|
value |
T |
required |
label |
String |
required |
state |
VdInputState |
.normal |
VdSelectionCard / VdSelectionCardGroup
Large tap-target selection cards, typically used for plan or feature selection.
String? _tier;
VdSelectionCardGroup<String>(
value: _tier,
onChanged: (v) => setState(() => _tier = v),
child: Column(
children: [
VdSelectionCardOption(
value: 'starter',
title: 'Starter',
description: 'Up to 3 projects',
leadingIcon: VdIcon(VdIcons.lightning_bolt),
),
VdSelectionCardOption(
value: 'growth',
title: 'Growth',
description: 'Unlimited projects + analytics',
leadingIcon: VdIcon(VdIcons.chart_line),
),
],
),
)
VdCodeInput
A PIN / OTP code entry field with configurable digit count.
VdCodeInput(
length: 6,
onCompleted: (code) => _verify(code),
)
| Parameter | Type | Default |
|---|---|---|
length |
int |
4 |
state |
VdInputState |
.normal |
onCompleted |
ValueChanged<String>? |
— |
onChanged |
ValueChanged<String>? |
— |
Light / Dark Mode
VdColorScheme.of(context) reads Theme.of(context).brightness. Components pick up the correct palette automatically — no extra work needed.
// Force light in a specific subtree:
Theme(
data: ThemeData(brightness: Brightness.light),
child: VdAlert(description: 'Always light'),
)
All token-level color choices (background, content, border) flip consistently between light and dark. There are no hard-coded Color literals in any component.