Skip to content
Kinetixui

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-flutter

Wrap 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.dart

That 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:flutter

Using 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:

FamilyAPIExample
ColoursKinetixColors.light / .darkKinetixColors.light.action
TypographyKinetixTypeKinetixType.bodyMd
SpacingKinetixSpacingKinetixSpacing.space4 → 16
RadiusKinetixRadiusKinetixRadius.container → 12
ShadowsKinetixShadow / KinetixShadowsKinetixShadow.md
MotionKinetixDuration, KinetixEasingKinetixDuration.fast
OpacityKinetixOpacityKinetixOpacity.disabled
Z-indexKinetixZIndexKinetixZIndex.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 (buttonVariants CVA 1:1), KinetixCheckbox, KinetixSwitch, KinetixToggle / KinetixToggleGroup, KinetixSlider, KinetixRadioGroup / KinetixRadioButton, KinetixRating, KinetixFab, KinetixStepper.
  • Inputs — KinetixInput, KinetixTextarea, KinetixNumberInput, KinetixPasswordInput, KinetixInputOtp, KinetixInputGroup family, KinetixSelect, KinetixDatePicker, KinetixCalendar, KinetixFileUpload, KinetixField family.
  • Display — KinetixBadge, KinetixTag, KinetixLabel, KinetixKbd (+ KbdGroup), KinetixSeparator, KinetixSkeleton, KinetixSpinner, KinetixProgress, KinetixCircularProgress, KinetixAvatar family, KinetixImage, KinetixCard family, KinetixAlert family, KinetixInform, KinetixQuote, KinetixMetric, KinetixCodeBlock, KinetixChart, KinetixAudioPlayer, KinetixAspectRatio, KinetixEmpty family.
  • Data — KinetixTable family, KinetixDataTable, KinetixCarousel.
  • Navigation & disclosure — KinetixAccordion family, KinetixCollapsible, KinetixTabsList / Trigger / Content, KinetixList / KinetixListItem, KinetixScrollArea, KinetixResizablePanels, KinetixBreadcrumb family, KinetixPagination family, KinetixTableOfContents, KinetixNavigationBar, KinetixAppBar / Link, KinetixTabBar / Item, KinetixFooter family.
  • Overlays & menus — KinetixDialog family, KinetixAlertDialog, KinetixModal, KinetixSheet, KinetixDrawer, KinetixPopover, KinetixHoverCard, KinetixTooltip, KinetixDropdownMenu (+ shared KinetixMenuItem / Separator / Label), KinetixContextMenu, KinetixMenubar / Menu, KinetixCommandDialog family, 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 no react-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; compose KinetixPopover + a filtered list, or reach for KinetixSelect.
  • 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 from Select's custom popover; KinetixSelect already wraps Flutter's own MenuAnchor.
  • AvatarGroup — re-wraps its children, not an idiomatic Flutter pattern; compose a Stack of KinetixAvatars shifted with Transform.
  • Tour — targets an arbitrary already-rendered element by CSS selector; Flutter has no equivalent live-tree query. Compose a sequence of KinetixPopover steps, 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. Use Draggable / DragTarget directly for the specific board.

Known gaps#

  • Shadow tokens are Flutter-first. KinetixShadow / KinetixShadowDark are generated for Flutter and for the web (--shadow-* in extras.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 to KinetixShadow.swift / .kt is a follow-up.
  • KinetixSidebar is 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, use NavigationRail directly.
  • No remote publishing. flutter analyze on ubuntu-latest (subosito/flutter-action) is the sole compiler feedback on every push that touches packages/ui-flutter, tokens/, or style-dictionary/.
  • Platform widgets are reused where they're strong — KinetixSlider, KinetixCalendar, KinetixDatePicker, KinetixCarousel, KinetixTooltip and the menu widgets wrap the Flutter built-in (Slider, CalendarDatePicker, showDatePicker, PageView, Tooltip, MenuAnchor / MenuBar), re-tinted onto the tokens. KinetixCalendar is single-date only.
  • KinetixChart is bar-only — a hand-drawn CustomPaint over the --chart-1…5 palette (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 vendored app_text.dart (KinetixType is a typedef alias); the scale is exported for your own text. A handful of 13px small-captions stay literal (no 13px step on the M3 scale).