refraction_ui 0.46.0 copy "refraction_ui: ^0.46.0" to clipboard
refraction_ui: ^0.46.0 copied to clipboard

A headless, highly customizable, and fully accessible Flutter UI library matching Refraction UI.

refraction_ui #

pub package pub points pub likes License: MIT CI

A headless, fully accessible, token-driven Flutter UI library. The Flutter chapter of Refraction UI — the same primitives you use on web (React / Astro) and on mobile, sharing a single design-token model so an app can match its brand exactly across every surface.

Try it live: https://elloloop.github.io/refraction-ui/flutter/

The link above runs the Flutter example app compiled to Flutter Web — every component below is interactive, with light/dark theming and a developer tools panel for poking at tokens.

dependencies:
  refraction_ui: ^0.2.0

Why refraction_ui #

  • Headless first. Every widget exposes the right hooks for behavior + accessibility, but visual style is driven entirely by RefractionThemeData. Re-skin the entire library in one place — no per-widget overrides, no global Theme patching.
  • One token model, every framework. The same color/radius/typography scale powers @refraction-ui/react, @refraction-ui/astro, and this Flutter package. Mobile and web stay in lockstep without duplicated theme definitions.
  • Accessible by default. Components ship with proper Semantics, focus traversal, and keyboard handling. Buttons announce their pressed/disabled state, inputs are labeled, dialogs trap focus, etc.
  • Pure Flutter. No platform channels, no native plugins, no FFI. Just widgets — works everywhere Flutter does (iOS, Android, web, macOS, Windows, Linux).
  • Zero deps beyond Flutter. The package's only runtime dependency is the Flutter SDK.

Live demo #

Every component, in both light and dark themes, with code excerpts and developer tools to inspect tokens:

https://elloloop.github.io/refraction-ui/flutter/

The live demo is built from the example/ folder in the source repo and is rebuilt and redeployed on every push to main.


Components #

All ~40 components below are token-driven and read from RefractionTheme.of(context).

Layout & navigation #

Widget Purpose
RefractionSidebar Persistent side navigation with collapsible groups
RefractionTabs Top-bar / segmented tab controller
RefractionAccordion Disclosure list with optional allowMultiple
RefractionBreadcrumbs Trail navigation with overflow handling
RefractionBottomNav Mobile bottom navigation
RefractionNavbar App-bar with leading/trailing slots

Inputs & forms #

Widget Purpose
RefractionInput Text input with label, helper, error states
RefractionSelect Single-value dropdown
RefractionCheckbox Tri-state checkbox (true / false / null)
RefractionRadioGroup Grouped radio buttons with keyboard nav
RefractionSwitch Animated on/off toggle
RefractionOtpInput One-time code with auto-advance + paste support
RefractionRichChatInput Multi-line input with action slots (send, attach, mic)
RefractionComposer Chat composer: auto-grow, IME-safe Enter-to-send, mention/slash/emoji/tag trigger suggestions with atomic tokens, attachments, drafts, edit-in-place

Buttons & menus #

Widget Purpose
RefractionButton Variants: primary, secondary, destructive, outline, ghost, link. Sizes: sm / default / lg / icon
RefractionCommandMenu Searchable command palette (cmd-K style)
RefractionDropdown Context menu / overflow menu

Feedback #

Widget Purpose
RefractionAlert Inline banner (info, success, warning, destructive)
RefractionToast Transient notification with auto-dismiss
RefractionTooltip Hover/focus tooltip
RefractionSkeleton Pulsing placeholder for loading states
RefractionProgress Determinate / indeterminate progress bar
RefractionSlider Single-thumb range input

Identity #

Widget Purpose
RefractionAvatar Image with fallback initials
RefractionBadge Compact label (primary, secondary, destructive, outline)

Theming #

Widget / class Purpose
RefractionTheme InheritedWidget exposing tokens to descendants
RefractionThemeData The bag of tokens (colors, radii, text style, spacing)
RefractionColors Semantic color palette (primary, destructive, accent, …)

Quick start #

import 'package:flutter/material.dart';
import 'package:refraction_ui/refraction_ui.dart';

void main() => runApp(const App());

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: RefractionTheme(
        data: RefractionThemeData.light(),
        child: const HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RefractionButton(
          variant: RefractionButtonVariant.primary,
          onPressed: () => debugPrint('clicked'),
          child: const Text('Get started'),
        ),
      ),
    );
  }
}

That's the whole setup. Wrap once in RefractionTheme, use widgets anywhere below.


Theming #

Theming is the heart of refraction_ui. There is one place to configure how everything looks: the RefractionThemeData you pass to RefractionTheme. No widget accepts ad-hoc style props that override theme tokens — if you want a change, you change a token, and every widget that uses it updates.

Use a built-in theme #

RefractionTheme(
  data: RefractionThemeData.light(),  // or .dark()
  child: child,
)

Override individual tokens #

RefractionTheme(
  data: RefractionThemeData.light().copyWith(
    colors: const RefractionColors.light().copyWith(
      primary: const Color(0xFF6366F1),         // brand indigo
      primaryForeground: Colors.white,
    ),
    borderRadius: 12.0,
    textStyle: GoogleFonts.inter(),
  ),
  child: child,
)

Switch theme at runtime #

class _AppState extends State<App> {
  Brightness _brightness = Brightness.light;

  @override
  Widget build(BuildContext context) {
    final data = _brightness == Brightness.light
        ? RefractionThemeData.light()
        : RefractionThemeData.dark();

    return RefractionTheme(
      data: data,
      child: AnimatedSwitcher(
        duration: const Duration(milliseconds: 200),
        child: HomePage(
          onToggleTheme: () => setState(() {
            _brightness = _brightness == Brightness.light
                ? Brightness.dark
                : Brightness.light;
          }),
        ),
      ),
    );
  }
}

Mirror your web brand exactly #

Because the React/Astro packages and this Flutter package consume the same token names (primary, primaryForeground, destructive, border, radius-md, …), you can keep mobile and web visually identical by exporting one source of truth (e.g. a JSON file) and feeding it into both:

// Web team's tokens.json -> these RefractionColors values
RefractionColors.light().copyWith(
  primary: Color(0xFF<from-tokens.json>),
  // …
)

Recipes #

Form with validation #

class SignUpForm extends StatefulWidget {
  const SignUpForm({super.key});
  @override
  State<SignUpForm> createState() => _SignUpFormState();
}

class _SignUpFormState extends State<SignUpForm> {
  final _email = TextEditingController();
  String? _error;

  void _submit() {
    setState(() {
      _error = _email.text.contains('@') ? null : 'Enter a valid email';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        RefractionInput(
          controller: _email,
          placeholder: 'name@example.com',
          errorText: _error,
        ),
        const SizedBox(height: 12),
        RefractionButton(
          variant: RefractionButtonVariant.primary,
          onPressed: _submit,
          child: const Text('Sign up'),
        ),
      ],
    );
  }
}

Loading state with Skeleton #

FutureBuilder<List<Item>>(
  future: api.fetchItems(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) {
      return Column(
        children: List.generate(5, (_) => const Padding(
          padding: EdgeInsets.symmetric(vertical: 6),
          child: RefractionSkeleton(height: 56, width: double.infinity),
        )),
      );
    }
    return ItemList(items: snapshot.data!);
  },
)

Toast notifications #

RefractionButton(
  onPressed: () => RefractionToast.of(context).show(
    title: 'Saved',
    description: 'Your changes are live.',
    variant: RefractionToastVariant.success,
  ),
  child: const Text('Save'),
)

Command palette (cmd-K) #

RefractionCommandMenu(
  items: [
    CommandItem(label: 'New file',     icon: Icons.add,    onSelect: _newFile),
    CommandItem(label: 'Open project', icon: Icons.folder, onSelect: _open),
    CommandItem(label: 'Settings',     icon: Icons.tune,   onSelect: _settings),
  ],
)

More recipes — including a complete family-calendar app and a pregnancy tracker built entirely with refraction_ui widgets — live under example/lib/apps/.


Compatibility #

Requirement Version
Dart SDK ^3.10.1
Flutter >= 1.17.0 (tested against current stable)
Platforms iOS, Android, Web, macOS, Windows, Linux

Null-safety is on. No platform channels — runs everywhere Flutter runs.


Sister packages (one design system, every framework) #

Package Framework npm / pub
refraction_ui (this) Flutter pub.dev/packages/refraction_ui
@refraction-ui/react React npm
@refraction-ui/astro Astro npm

Mix freely — the design tokens are shared, so a Next.js admin and a Flutter mobile app speak the same visual language.



Contributing #

PRs welcome. Run the package's tests with:

cd packages/flutter
flutter test
dart analyze

The repo's full CI (lint + unit tests + visual regression) runs on every PR via .github/workflows/ci.yml and .github/workflows/test-matrix.yml.


Emoji, stickers & attribution #

RefractionEmojiPicker ships the complete Unicode emoji set (~1,870 glyphs) as a single generated dataset (lib/src/data/emoji_dataset.g.dart, produced by tool/generate_emoji_data.dart) — the same source the composer's :shortcode: resolver consumes, so a picker tap and a :code: always agree.

By default the picker renders emoji with the bundled, platform-uniform Twemoji set (SVG, ~3.2 MB under assets/twemoji/) so emoji look identical on every OS and version — instead of the host platform's native glyphs, which differ. Rendering goes through a single EmojiRenderer seam, so you can swap it:

RefractionEmojiPicker(
  // Uniform Twemoji is the default; force native OS glyphs like this:
  emojiRenderer: defaultEmojiRenderer,
  // ...or supply your own (Noto / Fluent / your asset set).
)

A small starter sticker pack (refractionStarterStickers()) ships too — eight static SVG stickers plus one Lottie-animated sticker — proving both the static and animated render paths. Hosts add their own packs via RefractionEmojiPicker(stickers: ...).

Twemoji is licensed under CC-BY 4.0. Copyright 2020 Twitter, Inc and other contributors. See NOTICE. If you switch the renderer away from Twemoji, that attribution requirement no longer applies to your build.

License #

MIT © elloloop. See LICENSE.

1
likes
0
points
355
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

A headless, highly customizable, and fully accessible Flutter UI library matching Refraction UI.

Homepage
Repository (GitHub)
View/report issues

Topics

#ui #widget #design-system #components #theme

License

unknown (license)

Dependencies

characters, flutter, flutter_markdown, flutter_svg, lottie, markdown, video_player

More

Packages that depend on refraction_ui