Flutter
packages/ui-flutter is the Flutter port of KinetixUI — the fourth
platform alongside React (@kinetixui/ui),
Jetpack Compose and SwiftUI.
Kinetix* widgets for 90 of the
98 React components — the full React component
surface bar the eight deliberate non-ports, built in batches on a CI flutter analyze path;
each mirrors its packages/ui/src/components/*.tsx counterpart 1:1, with
a doc comment stating anything not carried over.
It's a standalone Flutter package inside this monorepo, not a
pnpm/npm workspace member — there's no package.json, so it's invisible
to pnpm install / Turborepo, exactly like packages/ui-compose
(Gradle) and packages/ui-swiftui (SwiftPM). It lives here for proximity
to the token source it depends on.
Not on pub.dev (publish_to: none). Consume it from a repo checkout
with a path: dependency — the same "own the code" model as the other
ports.
Setup#
pubspec.yaml targets sdk: ">=3.6.0 <4.0.0" / flutter: ">=3.27.0".
Add it via a path dependency:
dependencies:
kinetix_ui:
path: ../kinetixui/packages/ui-flutterWrap your app — or a screen — in KinetixTheme, then read colours with
KinetixTheme.of(context):
import 'package:flutter/material.dart';
import 'package:kinetix_ui/kinetix_ui.dart';
class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) {
return KinetixTheme(
// Picks light/dark off the platform brightness — pass
// `brightness:` to force it.
child: Column(
children: [
KinetixButton(onPressed: () {}, child: const Text('Save')),
KinetixButton(
variant: KinetixButtonVariant.outline,
size: KinetixButtonSize.sm,
onPressed: () {},
child: const Text('Cancel'),
),
],
),
);
}
}A custom theme#
KinetixTheme.custom takes your own palettes and still selects between them on
brightness:
KinetixTheme.custom(
light: AcmeTheme.light,
dark: AcmeTheme.dark,
child: App(),
)Material and Cupertino widgets get the same palette through the adapters:
MaterialApp(
theme: KinetixMaterialTheme.fromColors(Brightness.light, AcmeTheme.light),
darkTheme: KinetixMaterialTheme.fromColors(Brightness.dark, AcmeTheme.dark),
)
CupertinoApp(
theme: KinetixCupertinoTheme.fromColors(Brightness.light, AcmeTheme.light),
)Write that pair by hand, or export one from a Create design:
kinetixui preset flutter KX1_… --name AcmeTheme --output lib/acme_theme.dartThat writes a Dart class with a light and a dark KinetixColors, built from
the same resolved theme the workspace exports — see
the CLI reference.
Or skip the terminal: Create's Export panel has a Flutter target that generates the same file in the browser. Flutter is the most complete of the three colour targets — every role Create models reaches it.
Colours only, and the adapters map a subset. KinetixColors carries the
whole semantic contract, so KinetixTheme.custom gives Kinetix* widgets every
role. Material's ColorScheme and CupertinoThemeData are smaller surfaces, so
fromColors carries what they can honestly represent and no more — the mapping
is unchanged from light() / dark(). Radius and elevation are not themeable at
runtime at all: widgets read KinetixRadius constants directly, so a design's
radius and surface treatment apply on the web only.
The generated fields are static final rather than static const, because Dart
constant expressions do not include instance field access and the file references
KinetixColors.light.… for values the design left unchanged. It still works
anywhere a const theme would; you just cannot write const in front of
KinetixTheme.custom(…).
Tokens: a real dark pass, no name collision#
The pre-existing Flutter token output (packages/tokens/dist/flutter/:
app_colors.dart, app_theme.dart, app_text.dart) is light-only
and uses the class names KinetixColors / KinetixTheme — which would
collide with this package's KinetixColors value type and KinetixTheme
widget. So the token engine emits an additive Flutter-Color
semantic set for this library — KinetixColorScheme (light) /
KinetixColorSchemeDark (dark) — from the flutter-color-scheme
platform block in style-dictionary/sd.config.mjs, running on both
Style Dictionary passes (including the --chart-1…5 palette exposed as
KinetixColors.chart). The existing three files are byte-identical and
untouched. Same additive split as the SwiftUI KinetixColorsSwiftUI
output.
After any token change:
pnpm build:tokens && pnpm vendor:flutterUsing KinetixUI without Kinetix widgets#
You don't have to adopt the widgets to get the design language. The token families and the theme adapters are first-class, and all three approaches below read exactly the same generated tokens.
Foundation tokens#
Every family is a plain constant — no widget, no KinetixTheme, no
BuildContext:
| Family | API | Example |
|---|---|---|
| Colours | KinetixColors.light / .dark | KinetixColors.light.action |
| Typography | KinetixType | KinetixType.bodyMd |
| Spacing | KinetixSpacing | KinetixSpacing.space4 → 16 |
| Radius | KinetixRadius | KinetixRadius.container → 12 |
| Shadows | KinetixShadow / KinetixShadows | KinetixShadow.md |
| Motion | KinetixDuration, KinetixEasing | KinetixDuration.fast |
| Opacity | KinetixOpacity | KinetixOpacity.disabled |
| Z-index | KinetixZIndex | KinetixZIndex.overlay |
Container(
padding: const EdgeInsets.all(KinetixSpacing.space4),
decoration: BoxDecoration(
color: KinetixColors.light.card,
borderRadius: BorderRadius.circular(KinetixRadius.container),
boxShadow: KinetixShadow.md,
),
child: Text('No Kinetix widget here', style: KinetixType.bodyMd),
)Spacing and radius are generated into kinetix_motion.dart alongside the motion
scale — one generator emits the whole theme-independent foundation — and both
are exported from package:kinetix_ui/kinetix_ui.dart like everything else.
KinetixShadow carries the sm / md / lg / xl elevation scale and the
focus* rings as List<BoxShadow>, so multi-layer tokens stay multi-layer.
Only the focus rings change with brightness — the elevation scale is
theme-independent black-alpha — so reach for KinetixShadows.forBrightness(…)
or KinetixShadows.of(context) when you need the theme-correct ring.
Material#
MaterialApp(
theme: KinetixMaterialTheme.light(),
darkTheme: KinetixMaterialTheme.dark(),
)primary / onPrimary come from action / actionForeground (the interactive
fill is the role Material's primary actually plays), primaryContainer from
primary, secondaryContainer from accent, tertiary from brand, error
from destructive, surface / onSurface from background / foreground,
outline from border. The Kinetix palette is larger than Material's role set,
so link, focus, ring, actionHover, actionPressed, muted*,
tertiary*, warning*, success*, info*, card*, popover* and chart
have no Material slot and stay on KinetixColors. The type scale maps 1:1 onto
Material 3's 15 TextTheme slots.
ThemeData has no global spacing scale, so padding stays explicit via
KinetixSpacing — that's a deliberate split, not a gap. cardTheme,
dialogTheme, inputDecorationTheme and appBarTheme are deliberately not
themed: Flutter renamed those slots' types in 3.32+ (CardTheme →
CardThemeData, …) and flutter analyze fails on deprecations, so there's no
spelling correct across supported versions. Style those with KinetixRadius.
Cupertino#
CupertinoApp(
theme: KinetixCupertinoTheme.light(),
)Maps what Cupertino has — brightness, primaryColor,
primaryContrastingColor, scaffoldBackgroundColor, barBackgroundColor and
the text theme. Cupertino widgets keep their native behaviour; the adapter
changes colour and type, it doesn't make them look like Material.
Mixed#
MaterialApp(
theme: KinetixMaterialTheme.light(),
darkTheme: KinetixMaterialTheme.dark(),
home: KinetixTheme(child: /* Kinetix + native widgets together */),
)KinetixTheme resolves brightness from MediaQuery.platformBrightness, so it
follows the system automatically. If you drive brightness yourself with
themeMode, pass it through — KinetixTheme(brightness: Theme.of(context).brightness, …)
— so both systems agree.
What's covered#
KinetixTheme plus the Kinetix* widgets:
- Controls —
KinetixButton(buttonVariantsCVA 1:1),KinetixCheckbox,KinetixSwitch,KinetixToggle/KinetixToggleGroup,KinetixSlider,KinetixRadioGroup/KinetixRadioButton,KinetixRating,KinetixFab,KinetixStepper. - Inputs —
KinetixInput,KinetixTextarea,KinetixNumberInput,KinetixPasswordInput,KinetixInputOtp,KinetixInputGroupfamily,KinetixSelect,KinetixDatePicker,KinetixCalendar,KinetixFileUpload,KinetixFieldfamily. - Display —
KinetixBadge,KinetixTag,KinetixLabel,KinetixKbd(+KbdGroup),KinetixSeparator,KinetixSkeleton,KinetixSpinner,KinetixProgress,KinetixCircularProgress,KinetixAvatarfamily,KinetixImage,KinetixCardfamily,KinetixAlertfamily,KinetixInform,KinetixQuote,KinetixMetric,KinetixCodeBlock,KinetixChart,KinetixAudioPlayer,KinetixAspectRatio,KinetixEmptyfamily. - Data —
KinetixTablefamily,KinetixDataTable,KinetixCarousel. - Navigation & disclosure —
KinetixAccordionfamily,KinetixCollapsible,KinetixTabsList/Trigger/Content,KinetixList/KinetixListItem,KinetixScrollArea,KinetixResizablePanels,KinetixBreadcrumbfamily,KinetixPaginationfamily,KinetixTableOfContents,KinetixNavigationBar,KinetixAppBar/Link,KinetixTabBar/Item,KinetixFooterfamily. - Overlays & menus —
KinetixDialogfamily,KinetixAlertDialog,KinetixModal,KinetixSheet,KinetixDrawer,KinetixPopover,KinetixHoverCard,KinetixTooltip,KinetixDropdownMenu(+ sharedKinetixMenuItem/Separator/Label),KinetixContextMenu,KinetixMenubar/Menu,KinetixCommandDialogfamily,KinetixToaster.
Selection / expansion / presentation state is caller-owned — value +
ValueChanged<T> on the controls, a plain visible / expanded flag on
overlays and disclosures. Overlay widgets (KinetixDialog,
KinetixSheet, KinetixToaster, …) are placed in a top-level Stack.
Not ported (deliberate)#
Form—KinetixField(+Label/Description/Message) is the equivalent; there's noreact-hook-form-shaped context.NavigationMenu— a hover-triggered desktop mega-menu, no touch idiom.Combobox— a recipe (Popover + Command) on the web, not a component; composeKinetixPopover+ a filtered list, or reach forKinetixSelect.DirectionProvider— the web needs a provider to thread direction through Radix; this platform already carries layout direction in the framework itself, so there is nothing to wrap.NativeSelect— wraps the browser's own<select>, a web-only escape hatch fromSelect's custom popover;KinetixSelectalready wraps Flutter's ownMenuAnchor.AvatarGroup— re-wraps its children, not an idiomatic Flutter pattern; compose aStackofKinetixAvatars shifted withTransform.Tour— targets an arbitrary already-rendered element by CSS selector; Flutter has no equivalent live-tree query. Compose a sequence ofKinetixPopoversteps, each anchored to the widget it explains.KanbanBoard— built on@dnd-kit's accessible multi-container drag-and-drop; hand-rolling that from scratch is a much bigger lift than porting the component. UseDraggable/DragTargetdirectly for the specific board.
Known gaps#
- Shadow tokens are Flutter-first.
KinetixShadow/KinetixShadowDarkare generated for Flutter and for the web (--shadow-*inextras.css). SwiftUI and Jetpack Compose still have no generated shadow output — their ports hand-write the few shadows they use. That's a gap in the token engine, not in Flutter; porting the same generator toKinetixShadow.swift/.ktis a follow-up. KinetixSidebaris a scoped port — like SwiftUI and Compose it is the slide-in nav drawer (scrim,visible/onDismiss,KinetixSidebarGroup,KinetixSidebarMenuItem) and drops the desktop shell: rail / icon-collapse mode,SidebarInset, the cookie and the keyboard shortcut. For a persistent desktop rail, useNavigationRaildirectly.- No remote publishing.
flutter analyzeonubuntu-latest(subosito/flutter-action) is the sole compiler feedback on every push that touchespackages/ui-flutter,tokens/, orstyle-dictionary/. - Platform widgets are reused where they're strong —
KinetixSlider,KinetixCalendar,KinetixDatePicker,KinetixCarousel,KinetixTooltipand the menu widgets wrap the Flutter built-in (Slider,CalendarDatePicker,showDatePicker,PageView,Tooltip,MenuAnchor/MenuBar), re-tinted onto the tokens.KinetixCalendaris single-date only. KinetixChartis bar-only — a hand-drawnCustomPaintover the--chart-1…5palette (the package takes no charting dependency). Line / area kinds are a follow-up.- Type scale is wired. The widgets' text uses
AppText.<style>.copyWith(color: …)from the vendoredapp_text.dart(KinetixTypeis atypedefalias); the scale is exported for your own text. A handful of 13px small-captions stay literal (no 13px step on the M3 scale).