# Getting started Welcome to Shadcn UI for Flutter. This is the official documentation for Shadcn UI for Flutter. ![Shadcn UI](/shadcn-banner.png) > The work is still in progress. :::tip[AI Editor Integration] If you want to consume the docs in a LLM that accepts markdown, you can use this link: ::: ## Installation Run this command in your terminal from your project root directory: ```bash flutter pub add shadcn_ui ``` or manually adding to your `pubspec.yaml`: ```diff lang="yaml" dependencies: + shadcn_ui: ^0.2.4 # replace with the latest version ``` ## Shadcn (pure) Use the `ShadApp` widget if you want to use just the ShadcnUI components, without Material or Cupertino. ```diff lang="dart" + import 'package:shadcn_ui/shadcn_ui.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { + return ShadApp(); } ``` :::tip If you need to use the `Router` instead of the `Navigator`, use `ShadApp.router`. ::: ## Shadcn + Material We are the first Flutter UI library to allow shadcn components to be used simultaneously with Material components. The setup is simple: ```diff lang="dart" import 'package:shadcn_ui/shadcn_ui.dart'; + import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { - return ShadApp(); + return ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: (context) { + return MaterialApp( + theme: Theme.of(context), + builder: (context, child) { + return ShadAppBuilder(child: child!); + }, + ); + }, + ); } ``` :::tip If you need to use the `Router` instead of the `Navigator`, use `MaterialApp.router`. ::: --- The default Material `ThemeData` created by `ShadApp` is: ```dart ThemeData( fontFamily: themeData.textTheme.family, extensions: themeData.extensions, colorScheme: ColorScheme( brightness: themeData.brightness, primary: themeData.colorScheme.primary, onPrimary: themeData.colorScheme.primaryForeground, secondary: themeData.colorScheme.secondary, onSecondary: themeData.colorScheme.secondaryForeground, error: themeData.colorScheme.destructive, onError: themeData.colorScheme.destructiveForeground, surface: themeData.colorScheme.background, onSurface: themeData.colorScheme.foreground, ), scaffoldBackgroundColor: themeData.colorScheme.background, brightness: themeData.brightness, dividerTheme: DividerThemeData( color: themeData.colorScheme.border, thickness: 1, ), textSelectionTheme: TextSelectionThemeData( cursorColor: themeData.colorScheme.primary, selectionColor: themeData.colorScheme.selection, selectionHandleColor: themeData.colorScheme.primary, ), iconTheme: IconThemeData( size: 16, color: themeData.colorScheme.foreground, ), scrollbarTheme: ScrollbarThemeData( crossAxisMargin: 1, mainAxisMargin: 1, thickness: const WidgetStatePropertyAll(8), radius: const Radius.circular(999), thumbColor: WidgetStatePropertyAll(themeData.colorScheme.border), ), ), ``` :::note Use `Theme.of(context).copyWith(...)` to override the default theme, without losing the default values provided by shadcn_ui. ::: ## Shadcn + Cupertino If you need to use shadcn components with Cupertino components, use `CupertinoApp` instead of `MaterialApp`, like you are already used to. ```diff lang="dart" import 'package:shadcn_ui/shadcn_ui.dart'; + import 'package:flutter/cupertino.dart'; + import 'package:flutter_localizations/flutter_localizations.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { - return ShadApp(); + return ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: (context) { + return CupertinoApp( + theme: CupertinoTheme.of(context), + localizationsDelegates: const [ + DefaultMaterialLocalizations.delegate, + DefaultCupertinoLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + builder: (context, child) { + return ShadAppBuilder(child: child!); + }, + ); + }, + ); } ``` :::tip If you need to use the `Router` instead of the `Navigator`, use `CupertinoApp.router`. ::: --- The default `CupertinoThemeData` created by `ShadApp` is: ```dart CupertinoThemeData( primaryColor: themeData.colorScheme.primary, primaryContrastingColor: themeData.colorScheme.primaryForeground, scaffoldBackgroundColor: themeData.colorScheme.background, barBackgroundColor: themeData.colorScheme.primary, brightness: themeData.brightness, ), ``` :::note Use `CupertinoTheme.of(context).copyWith(...)` to override the default theme, without losing the default values provided by shadcn_ui. ::: --- # Submit your app To add your app to the Shadcn UI for Flutter showcase, please go to the [following link](https://github.com/nank1ro/flutter-shadcn-ui/issues/new?template=docs-add-app.yaml) and fill out the form. --- # Theme Data Defines the theme and color scheme for the app. The supported color schemes are: - blue - gray - green - neutral - orange - red - rose - slate - stone - violet - yellow - zinc ## Usage ```diff lang="dart" import 'package:shadcn_ui/shadcn_ui.dart'; @override Widget build(BuildContext context) { return ShadApp( + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), child: ... ); } ``` You can override specific properties of the selected theme/color scheme: ```diff lang="dart" import 'package:shadcn_ui/shadcn_ui.dart'; @override Widget build(BuildContext context) { return ShadApp( darkTheme: ShadThemeData( brightness: Brightness.dark, colorScheme: const ShadSlateColorScheme.dark( + background: Colors.blue, ), + primaryButtonTheme: const ShadButtonTheme( + backgroundColor: Colors.cyan, + ), ), ), child: ... ); } ``` :::tip You can also create your custom color scheme, just extend the `ShadColorScheme` class and pass all the properties. ::: ## ShadColorScheme.fromName If you want to allow the user to change the default shadcn themes, I suggest using `ShadColorScheme.fromName`. ```dart // available color scheme names final shadThemeColors = [ 'blue', 'gray', 'green', 'neutral', 'orange', 'red', 'rose', 'slate', 'stone', 'violet', 'yellow', 'zinc', ]; final lightColorScheme = ShadColorScheme.fromName('blue'); final darkColorScheme = ShadColorScheme.fromName('slate', brightness: Brightness.dark); ``` In this way you can easily create a select to change the color scheme, for example: ```dart import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; // Somewhere in your app ShadSelect( initialValue: 'slate', maxHeight: 200, options: shadThemeColors.map( (option) => ShadOption( value: option, child: Text( option.capitalizeFirst(), ), ), ), selectedOptionBuilder: (context, value) { return Text(value.capitalizeFirst()); }, onChanged: (value) { // rebuild the app using your state management solution }, ), ``` For example I'm using solidart as state management, here it is the example code used to rebuild the app widget when the user changes the theme mode. Check the "Toggle Theme" example at The same can be done for the color scheme, using a `Signal()` ## Extend with custom colors You can extend the `ShadColorScheme` with your own custom colors by using the `custom` parameter. ```diff lang="dart" return ShadApp( theme: ShadThemeData( + colorScheme: const ShadZincColorScheme.light( + custom: { + 'myCustomColor': Color.fromARGB(255, 177, 4, 196), + }, + ), ), ); ``` Then you can access it like this `ShadTheme.of(context).colorScheme.custom['myCustomColor']!`. Or you can create an extension on `ShadColorScheme` to make it easier to access: ```dart extension CustomColorExtension on ShadColorScheme { Color get myCustomColor => custom['myCustomColor']!; } ``` In this way you can access it like other colors `ShadTheme.of(context).colorScheme.myCustomColor`. --- # Packages ## Packages included in the library Flutter Shadcn UI consists of fantastic open-source libraries that are exported and you can use them without importing them into your project. ### [flutter_animate](https://pub.dev/packages/flutter_animate) The flutter animate library is a very cool animations library extensively used in Shadcn UI Components. With flutter_animate animations can be easily customized from the user, because components will take a `List`. ### [lucide_icons_flutter](https://pub.dev/packages/lucide_icons_flutter) A nice icon library that is used in Shadcn UI Components. You can use Lucide icons with the `LucideIcons` class, for example `LucideIcons.activity`. You can browse all the icons [here](https://lucide.dev/icons/). ### [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables) A nice raw table (very performant) implementation used by the [ShadTable](../components/table) component. ### [intl](https://pub.dev/packages/intl) The intl package provides internationalization and localization facilities, including message translation. ### [universal_image](https://pub.dev/packages/universal_image) Support multiple image formats. Used by the [ShadAvatar](../components/avatar) component. --- # Typography Styles for headings, paragraphs, lists...etc ## h1Large ```dart Text( 'Taxing Laughter: The Joke Tax Chronicles', style: ShadTheme.of(context).textTheme.h1Large, ) ``` ## h1 ```dart Text( 'Taxing Laughter: The Joke Tax Chronicles', style: ShadTheme.of(context).textTheme.h1, ) ``` ## h2 ```dart Text( 'The People of the Kingdom', style: ShadTheme.of(context).textTheme.h2, ) ``` ## h3 ```dart Text( 'The Joke Tax', style: ShadTheme.of(context).textTheme.h3, ) ``` ## h4 ```dart Text( 'The king, seeing how much happier his subjects were, realized the error of his ways and repealed the joke tax.', style: ShadTheme.of(context).textTheme.h4, ) ``` ## p ```dart Text( 'The king, seeing how much happier his subjects were, realized the error of his ways and repealed the joke tax.', style: ShadTheme.of(context).textTheme.p, ) ``` ## Blockquote ```dart Text( '"After all," he said, "everyone enjoys a good joke, so it\'s only fair that they should pay for the privilege."', style: ShadTheme.of(context).textTheme.blockquote, ) ``` ## Table ```dart Text( "King's Treasury", style: ShadTheme.of(context).textTheme.table, ) ``` ## List ```dart Text( '1st level of puns: 5 gold coins', style: ShadTheme.of(context).textTheme.list, ) ``` ## Lead ```dart Text( 'A modal dialog that interrupts the user with important content and expects a response.', style: ShadTheme.of(context).textTheme.lead, ) ``` ## Large ```dart Text( 'Are you absolutely sure?', style: ShadTheme.of(context).textTheme.large, ) ``` ## Small ```dart Text( 'Email address', style: ShadTheme.of(context).textTheme.small, ) ``` ## Muted ```dart Text( 'Enter your email address.', style: ShadTheme.of(context).textTheme.muted, ) ``` ## Custom font family By default Shadcn UI uses [Geist](https://vercel.com/font) as default font family. To change it, add the local font to your project, for example in the `/fonts` directory. Then update your `pubspec.yaml` with something like this: ```diff lang="yaml" flutter: + fonts: + - family: UbuntuMono + fonts: + - asset: fonts/UbuntuMono-Regular.ttf + - asset: fonts/UbuntuMono-Italic.ttf + style: italic + - asset: fonts/UbuntuMono-Bold.ttf + weight: 700 + - asset: fonts/UbuntuMono-BoldItalic.ttf + weight: 700 + style: italic ``` Then in your `ShadApp` update the `ShadTextTheme`: ```diff lang="dart" return ShadApp( debugShowCheckedModeBanner: false, themeMode: themeMode, routes: routes, theme: ShadThemeData( brightness: Brightness.light, colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme( + colorScheme: const ShadZincColorScheme.light(), + family: 'UbuntuMono', + ), ), ... ); ``` ## Google font Install the [google_fonts](https://pub.dev/packages/google_fonts) package. Then add the google font to your `ShadApp`: ```diff lang="dart" return ShadApp( debugShowCheckedModeBanner: false, themeMode: themeMode, routes: routes, theme: ShadThemeData( brightness: Brightness.light, colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins), ), ... ); ``` ## Extend with custom styles You can extend the `ShadTextTheme` with your own custom styles by using the `custom` parameter. ```diff lang="dart" return ShadApp( theme: ShadThemeData( + textTheme: ShadTextTheme( + custom: { + 'myCustomStyle': const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.blue, + ), + }, + ), ), ); ``` Then you can access it like this `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`. Or you can create an extension on `ShadTextTheme` to make it easier to access: ```dart extension CustomStyleExtension on ShadTextTheme { TextStyle get myCustomStyle => custom['myCustomStyle']!; } ``` In this way you can access it like other styles `ShadTheme.of(context).textTheme.myCustomStyle`. --- # Accordion A vertically stacked set of interactive headings that each reveal a section of content. ```dart final details = [ ( title: 'Is it acceptable?', content: 'Yes. It adheres to the WAI-ARIA design pattern.', ), ( title: 'Is it styled?', content: "Yes. It comes with default styles that matches the other components' aesthetic.", ), ( title: 'Is it animated?', content: "Yes. It's animated by default, but you can disable it if you prefer.", ), ]; @override Widget build(BuildContext context) { return ShadAccordion<({String content, String title})>( children: details.map( (detail) => ShadAccordionItem( value: detail, title: Text(detail.title), child: Text(detail.content), ), ), ); } ``` ## Multiple ```dart final details = [ ( title: 'Is it acceptable?', content: 'Yes. It adheres to the WAI-ARIA design pattern.', ), ( title: 'Is it styled?', content: "Yes. It comes with default styles that matches the other components' aesthetic.", ), ( title: 'Is it animated?', content: "Yes. It's animated by default, but you can disable it if you prefer.", ), ]; @override Widget build(BuildContext context) { return ShadAccordion<({String content, String title})>.multiple( children: details.map( (detail) => ShadAccordionItem( value: detail, title: Text(detail.title), child: Text(detail.content), ), ), ); } ``` --- # Alert Displays a callout for user attention. ```dart ShadAlert( icon: Icon(LucideIcons.terminal), title: Text('Heads up!'), description: Text('You can add components to your app using the cli.'), ), ``` ## Destructive ```dart ShadAlert.destructive( icon: Icon(LucideIcons.circleAlert), title: Text('Error'), description: Text('Your session has expired. Please log in again.'), ) ``` --- # Avatar An image element with a placeholder for representing the user. ```dart ShadAvatar( 'https://app.requestly.io/delay/2000/avatars.githubusercontent.com/u/124599?v=4', placeholder: Text('CN'), ) ``` --- # Badge Displays a badge or a component that looks like a badge. ## Primary ```dart ShadBadge( child: const Text('Primary'), ) ``` ## Secondary ```dart ShadBadge.secondary( child: const Text('Secondary'), ) ``` ## Destructive ```dart ShadBadge.destructive( child: const Text('Destructive'), ) ``` ## Outline ```dart ShadBadge.outline( child: const Text('Outline'), ) ``` --- # Button Displays a button or a component that looks like a button. ## Primary ```dart ShadButton( child: const Text('Primary'), onPressed: () {}, ) ``` ## Secondary ```dart ShadButton.secondary( child: const Text('Secondary'), onPressed: () {}, ) ``` ## Destructive ```dart ShadButton.destructive( child: const Text('Destructive'), onPressed: () {}, ) ``` ## Outline ```dart ShadButton.outline( child: const Text('Outline'), onPressed: () {}, ) ``` ## Ghost ```dart ShadButton.ghost( child: const Text('Ghost'), onPressed: () {}, ) ``` ## Link ```dart ShadButton.link( child: const Text('Link'), onPressed: () {}, ) ``` ## Text and Icon ```dart ShadButton( onPressed: () {}, leading: const Icon(LucideIcons.mail), child: const Text('Login with Email'), ) ``` ## Loading ```dart ShadButton( onPressed: () {}, leading: SizedBox.square( dimension: 16, child: CircularProgressIndicator( strokeWidth: 2, color: ShadTheme.of(context).colorScheme.primaryForeground, ), ), child: const Text('Please wait'), ) ``` ## Gradient and Shadow ```dart ShadButton( onPressed: () {}, gradient: const LinearGradient(colors: [ Colors.cyan, Colors.indigo, ]), shadows: [ BoxShadow( color: Colors.blue.withOpacity(.4), spreadRadius: 4, blurRadius: 10, offset: const Offset(0, 2), ), ], child: const Text('Gradient with Shadow'), ) ``` --- # Calendar A date field component that allows users to enter and edit date. ```dart class SingleCalendar extends StatefulWidget { const SingleCalendar({super.key}); @override State createState() => _SingleCalendarState(); } class _SingleCalendarState extends State { final today = DateTime.now(); @override Widget build(BuildContext context) { return ShadCalendar( selected: today, fromMonth: DateTime(today.year - 1), toMonth: DateTime(today.year, 12), ); } } ``` ## Multiple ```dart class MultipleCalendar extends StatefulWidget { const MultipleCalendar({super.key}); @override State createState() => _MultipleCalendarState(); } class _MultipleCalendarState extends State { final today = DateTime.now(); @override Widget build(BuildContext context) { return ShadCalendar.multiple( numberOfMonths: 2, fromMonth: DateTime(today.year), toMonth: DateTime(today.year + 1, 12), min: 5, max: 10, ); } } ``` ## Range ```dart class RangeCalendar extends StatelessWidget { const RangeCalendar({super.key}); @override Widget build(BuildContext context) { return const ShadCalendar.range( min: 2, max: 5, ); } } ``` #### DropdownMonths ```dart ShadCalendar( captionLayout: ShadCalendarCaptionLayout.dropdownMonths, ); ``` #### DropdownYears ```dart ShadCalendar( captionLayout: ShadCalendarCaptionLayout.dropdownYears, ); ``` ### Hide Navigation ```dart ShadCalendar( hideNavigation: true, ); ``` ### Show Week Numbers ```dart ShadCalendar( showWeekNumbers: true, ); ``` ### Show Outside Days (false) ```dart ShadCalendar( showOutsideDays: false, ); ``` ### Fixed Weeks ```dart ShadCalendar( fixedWeeks: true, ); ``` ### Hide Weekday Names ```dart ShadCalendar( hideWeekdayNames: true, ); ``` --- # Card Displays a card with header, content, and footer. ```dart const frameworks = { 'next': 'Next.js', 'react': 'React', 'astro': 'Astro', 'nuxt': 'Nuxt.js', }; class CardProject extends StatelessWidget { const CardProject({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ShadCard( width: 350, title: Text('Create project', style: theme.textTheme.h4), description: const Text('Deploy your new project in one-click.'), footer: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ShadButton.outline( child: const Text('Cancel'), onPressed: () {}, ), ShadButton( child: const Text('Deploy'), onPressed: () {}, ), ], ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Name'), const SizedBox(height: 6), const ShadInput(placeholder: Text('Name of your project')), const SizedBox(height: 16), const Text('Framework'), const SizedBox(height: 6), ShadSelect( placeholder: const Text('Select'), options: frameworks.entries .map((e) => ShadOption(value: e.key, child: Text(e.value))) .toList(), selectedOptionBuilder: (context, value) { return Text(frameworks[value]!); }, onChanged: (value) {}, ), ], ), ), ); } } ``` ## Notifications Example ```dart import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; const notifications = [ ( title: "Your call has been confirmed.", description: "1 hour ago", ), ( title: "You have a new message!", description: "1 hour ago", ), ( title: "Your subscription is expiring soon!", description: "2 hours ago", ), ]; class CardNotifications extends StatefulWidget { const CardNotifications({super.key}); @override State createState() => _CardNotificationsState(); } class _CardNotificationsState extends State { final pushNotifications = ValueNotifier(false); @override void dispose() { pushNotifications.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ShadCard( width: 380, title: const Text('Notifications'), description: const Text('You have 3 unread messages.'), footer: ShadButton( width: double.infinity, leading: const Padding( padding: EdgeInsets.only(right: 8), child: Icon(LucideIcons.check), ), onPressed: () {}, child: const Text('Mark all as read'), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: theme.radius, border: Border.all(color: theme.colorScheme.border), ), child: Row( children: [ Icon( LucideIcons.bellRing, size: 24, color: theme.colorScheme.foreground, ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Push Notifications', style: theme.textTheme.small, ), const SizedBox(height: 4), Text( 'Send notifications to device.', style: theme.textTheme.muted, ) ], ), ), ), ValueListenableBuilder( valueListenable: pushNotifications, builder: (context, value, child) { return ShadSwitch( value: value, onChanged: (v) => pushNotifications.value = v, ); }, ), ], ), ), const SizedBox(height: 16), ...notifications .map( (n) => Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 8, height: 8, margin: const EdgeInsets.only(top: 4), decoration: const BoxDecoration( color: Color(0xFF0CA5E9), shape: BoxShape.circle, ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Your call has been confirmed.', style: theme.textTheme.small), const SizedBox(height: 4), Text(n.description, style: theme.textTheme.muted), ], ), ), ) ], ), ) .separatedBy(const SizedBox(height: 16)), const SizedBox(height: 16), ], ), ); } } ``` --- # Checkbox A control that allows the user to toggle between checked and not checked. ```dart class CheckboxSample extends StatefulWidget { const CheckboxSample({super.key}); @override State createState() => _CheckboxSampleState(); } class _CheckboxSampleState extends State { bool value = false; @override Widget build(BuildContext context) { return ShadCheckbox( value: value, onChanged: (v) => setState(() => value = v), label: const Text('Accept terms and conditions'), sublabel: const Text( 'You agree to our Terms of Service and Privacy Policy.', ), ); } } ``` ## Form ```dart ShadCheckboxFormField( id: 'terms', initialValue: false, inputLabel: const Text('I accept the terms and conditions'), onChanged: (v) {}, inputSublabel: const Text('You agree to our Terms and Conditions'), validator: (v) { if (!v) { return 'You must accept the terms and conditions'; } return null; }, ) ``` --- # Context Menu Displays a menu to the user — such as a set of actions or functions — triggered by a mouse right-click. ```dart import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; class ContextMenuPage extends StatelessWidget { const ContextMenuPage({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Scaffold( body: Padding( padding: const EdgeInsets.all(16), child: ShadContextMenuRegion( constraints: const BoxConstraints(minWidth: 300), items: [ const ShadContextMenuItem.inset( child: Text('Back'), ), const ShadContextMenuItem.inset( enabled: false, child: Text('Forward'), ), const ShadContextMenuItem.inset( child: Text('Reload'), ), const ShadContextMenuItem.inset( trailing: Icon(LucideIcons.chevronRight), items: [ ShadContextMenuItem( child: Text('Save Page As...'), ), ShadContextMenuItem( child: Text('Create Shortcut...'), ), ShadContextMenuItem( child: Text('Name Window...'), ), Divider(height: 8), ShadContextMenuItem( child: Text('Developer Tools'), ), ], child: Text('More Tools'), ), const Divider(height: 8), const ShadContextMenuItem( leading: Icon(LucideIcons.check), child: Text('Show Bookmarks Bar'), ), const ShadContextMenuItem.inset(child: Text('Show Full URLs')), const Divider(height: 8), Padding( padding: const EdgeInsets.fromLTRB(36, 8, 8, 8), child: Text('People', style: theme.textTheme.small), ), const Divider(height: 8), ShadContextMenuItem( leading: SizedBox.square( dimension: 16, child: Center( child: Container( width: 8, height: 8, decoration: BoxDecoration( color: theme.colorScheme.foreground, shape: BoxShape.circle, ), ), ), ), child: const Text('Pedro Duarte'), ), const ShadContextMenuItem.inset(child: Text('Colm Tuite')), ], child: Container( width: 300, height: 200, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.border), borderRadius: BorderRadius.circular(8), ), child: const Text('Right click here'), ), ), ), ); } } ``` --- # Date Picker A date picker component with range and presets. ```dart class SingleDatePicker extends StatelessWidget { const SingleDatePicker({super.key}); @override Widget build(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: const ShadDatePicker(), ); } } ``` ## Date Range Picker ```dart class RangeDatePicker extends StatelessWidget { const RangeDatePicker({super.key}); @override Widget build(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: const ShadDatePicker.range(), ); } } ``` ## With Presets ```dart const presets = { 0: 'Today', 1: 'Tomorrow', 3: 'In 3 days', 7: 'In a week', }; class PresetsDatePicker extends StatefulWidget { const PresetsDatePicker({super.key}); @override State createState() => _PresetsDatePickerState(); } class _PresetsDatePickerState extends State { final groupId = UniqueKey(); final today = DateTime.now().startOfDay; DateTime? selected; @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: ShadDatePicker( // Using the same groupId to keep the date picker popover open when the // select popover is closed. groupId: groupId, header: Padding( padding: const EdgeInsets.only(bottom: 4), child: ShadSelect( groupId: groupId, minWidth: 276, placeholder: const Text('Select'), options: presets.entries .map((e) => ShadOption(value: e.key, child: Text(e.value))) .toList(), selectedOptionBuilder: (context, value) { return Text(presets[value]!); }, onChanged: (value) { if (value == null) return; setState(() { selected = today.add(Duration(days: value)); }); }, ), ), selected: selected, calendarDecoration: theme.calendarTheme.decoration, popoverPadding: const EdgeInsets.all(4), ), ); } } ``` ## Form ```dart ShadDatePickerFormField( label: const Text('Date of birth'), onChanged: print, description: const Text( 'Your date of birth is used to calculate your age.'), validator: (v) { if (v == null) { return 'A date of birth is required.'; } return null; }, ), ``` ## DateRangePickerFormField ```dart ShadDateRangePickerFormField( label: const Text('Range of dates'), onChanged: print, description: const Text( 'Select the range of dates you want to search between.'), validator: (v) { if (v == null) return 'A range of dates is required.'; if (v.start == null) { return 'The start date is required.'; } if (v.end == null) return 'The end date is required.'; return null; }, ), ``` --- # Dialog A modal dialog that interrupts the user. ```dart import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; final profile = [ (title: 'Name', value: 'Alexandru'), (title: 'Username', value: 'nank1ro'), ]; class DialogExample extends StatelessWidget { const DialogExample({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ShadButton.outline( child: const Text('Edit Profile'), onPressed: () { showShadDialog( context: context, builder: (context) => ShadDialog( title: const Text('Edit Profile'), description: const Text( "Make changes to your profile here. Click save when you're done"), actions: const [ShadButton(child: Text('Save changes'))], child: Container( width: 375, padding: const EdgeInsets.symmetric(vertical: 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, spacing: 16, children: profile .map( (p) => Row( children: [ Expanded( child: Text( p.title, textAlign: TextAlign.end, style: theme.textTheme.small, ), ), const SizedBox(width: 16), Expanded( flex: 3, child: ShadInput(initialValue: p.value), ), ], ), ).toList(), ), ), ), ); }, ); } } ``` ## Alert ```dart class DialogExample extends StatelessWidget { const DialogExample({super.key}); @override Widget build(BuildContext context) { return ShadButton.outline( child: const Text('Show Dialog'), onPressed: () { showShadDialog( context: context, builder: (context) => ShadDialog.alert( title: const Text('Are you absolutely sure?'), description: const Padding( padding: EdgeInsets.only(bottom: 8), child: Text( 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.', ), ), actions: [ ShadButton.outline( child: const Text('Cancel'), onPressed: () => Navigator.of(context).pop(false), ), ShadButton( child: const Text('Continue'), onPressed: () => Navigator.of(context).pop(true), ), ], ), ); }, ); } } ``` --- # Form Builds a form with validation and easy access to form fields values. ```dart class FormPage extends StatefulWidget { const FormPage({ super.key, }); @override State createState() => _FormPageState(); } class _FormPageState extends State { final formKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ShadForm( key: formKey, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 350), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ShadInputFormField( id: 'username', label: const Text('Username'), placeholder: const Text('Enter your username'), description: const Text('This is your public display name.'), validator: (v) { if (v.length < 2) { return 'Username must be at least 2 characters.'; } return null; }, ), const SizedBox(height: 16), ShadButton( child: const Text('Submit'), onPressed: () { if (formKey.currentState!.saveAndValidate()) { print( 'validation succeeded with ${formKey.currentState!.value}'); } else { print('validation failed'); } }, ), ], ), ), ), ), ); } } ``` ## Examples See the following links for more examples on how to use the `ShadForm` component with other components: - [Checkbox](../checkbox#form) - [Switch](../switch#form) - [Input](../input#form) - [Select](../select#form) - [RadioGroup](../radio-group#form) - [DatePicker](../date-picker#form) - [TimePicker](../time-picker#form) --- # IconButton Displays an icon button or a component that looks like a button with an icon. ## Primary ```dart ShadIconButton( onPressed: () => print('Primary'), icon: const Icon(LucideIcons.rocket), ) ``` ## Secondary ```dart ShadIconButton.secondary( icon: const Icon(LucideIcons.rocket), onPressed: () => print('Secondary'), ) ``` ## Destructive ```dart ShadIconButton.destructive( icon: const Icon(LucideIcons.rocket), onPressed: () => print('Destructive'), ) ``` ## Outline ```dart ShadIconButton.outline( icon: const Icon(LucideIcons.rocket), onPressed: () => print('Outline'), ) ``` ## Ghost ```dart ShadIconButton.ghost( icon: const Icon(LucideIcons.rocket), onPressed: () => print('Ghost'), ) ``` ## Loading ```dart ShadIconButton( icon: SizedBox.square( dimension: 16, child: CircularProgressIndicator( strokeWidth: 2, color: ShadTheme.of(context).colorScheme.primaryForeground, ), ), ) ``` ## Gradient and Shadow ```dart ShadIconButton( gradient: const LinearGradient(colors: [ Colors.cyan, Colors.indigo, ]), shadows: [ BoxShadow( color: Colors.blue.withValues(alpha: .4), spreadRadius: 4, blurRadius: 10, offset: const Offset(0, 2), ), ], icon: const Icon(LucideIcons.rocket), ) ``` --- # Input Displays a form input field or a component that looks like an input field. ```dart ConstrainedBox( constraints: const BoxConstraints(maxWidth: 320), child: const ShadInput( placeholder: Text('Email'), keyboardType: TextInputType.emailAddress, ), ), ``` ## With leading and trailing ```dart class PasswordInput extends StatefulWidget { const PasswordInput({super.key}); @override State createState() => _PasswordInputState(); } class _PasswordInputState extends State { bool obscure = true; @override Widget build(BuildContext context) { return ShadInput( placeholder: const Text('Password'), obscureText: obscure, leading: const Padding( padding: EdgeInsets.all(4.0), child: Icon(LucideIcons.lock), ), trailing: ShadButton( width: 24, height: 24, padding: EdgeInsets.zero, icon: Icon(obscure ? LucideIcons.eyeOff : LucideIcons.eye), onPressed: () { setState(() => obscure = !obscure); }, ), ); } } ``` ## Form ```dart ShadInputFormField( id: 'username', label: const Text('Username'), placeholder: const Text('Enter your username'), description: const Text('This is your public display name.'), validator: (v) { if (v.length < 2) { return 'Username must be at least 2 characters.'; } return null; }, ), ``` --- # InputOTP Accessible one-time password component with copy paste functionality. ```dart ShadInputOTP( onChanged: (v) => print('OTP: $v'), maxLength: 6, children: const [ ShadInputOTPGroup( children: [ ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ], ), Icon(size: 24, LucideIcons.dot), ShadInputOTPGroup( children: [ ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ], ), ], ) ``` ## InputFormatters Using InputFormatters you can restrict the input characters. The example below shows how to restrict the input to only numbers. ```dart ShadInputOTP( onChanged: (v) => print('OTP: $v'), maxLength: 4, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], children: const [ ShadInputOTPGroup( children: [ ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ], ), ], ) ``` See also `UpperCaseTextInputFormatter` and `LowerCaseTextInputFormatter` which are provided by the package. ## Form ```dart ShadInputOTPFormField( id: 'otp', maxLength: 6, label: const Text('OTP'), description: const Text('Enter your OTP.'), validator: (v) { if (v.contains(' ')) { return 'Fill the whole OTP code'; } return null; }, children: const [ ShadInputOTPGroup( children: [ ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ], ), Icon(size: 24, LucideIcons.dot), ShadInputOTPGroup( children: [ ShadInputOTPSlot(), ShadInputOTPSlot(), ShadInputOTPSlot(), ], ), ], ) ``` --- # Menubar A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. ```dart class MenubarExample extends StatelessWidget { const MenubarExample({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); final square = SizedBox.square( dimension: 16, child: Center( child: SizedBox.square( dimension: 8, child: DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.foreground, shape: BoxShape.circle, ), ), ), ), ); final divider = ShadSeparator.horizontal( margin: const EdgeInsets.symmetric(vertical: 4), color: theme.colorScheme.muted, ); return ShadMenubar( items: [ ShadMenubarItem( items: [ const ShadContextMenuItem(child: Text('New Tab')), const ShadContextMenuItem(child: Text('New Window')), const ShadContextMenuItem( enabled: false, child: Text('New Incognito Window'), ), divider, const ShadContextMenuItem( trailing: Icon(LucideIcons.chevronRight), items: [ ShadContextMenuItem(child: Text('Email Link')), ShadContextMenuItem(child: Text('Messages')), ShadContextMenuItem(child: Text('Notes')), ], child: Text('Share'), ), divider, const ShadContextMenuItem(child: Text('Print...')), ], child: const Text('File'), ), ShadMenubarItem( items: [ const ShadContextMenuItem(child: Text('Undo')), const ShadContextMenuItem(child: Text('Redo')), divider, ShadContextMenuItem( trailing: const Icon(LucideIcons.chevronRight), items: [ const ShadContextMenuItem(child: Text('Search the web')), divider, const ShadContextMenuItem(child: Text('Find...')), const ShadContextMenuItem(child: Text('Find Next')), const ShadContextMenuItem(child: Text('Find Previous')), ], child: const Text('Find'), ), divider, const ShadContextMenuItem(child: Text('Cut')), const ShadContextMenuItem(child: Text('Copy')), const ShadContextMenuItem(child: Text('Paste')), ], child: const Text('Edit'), ), ShadMenubarItem( items: [ const ShadContextMenuItem.inset( child: Text('Always Show Bookmarks Bar'), ), const ShadContextMenuItem( leading: Icon(LucideIcons.check), child: Text('Always Show Full URLs'), ), divider, const ShadContextMenuItem.inset(child: Text('Reload')), const ShadContextMenuItem.inset( enabled: false, child: Text('Force Reload')), divider, const ShadContextMenuItem.inset( child: Text('Toggle Full Screen'), ), divider, const ShadContextMenuItem.inset(child: Text('Hide Sidebar')), ], child: const Text('View'), ), ShadMenubarItem(items: [ const ShadContextMenuItem.inset(child: Text('Andy')), ShadContextMenuItem(leading: square, child: const Text('Benoit')), const ShadContextMenuItem.inset(child: Text('Luis')), divider, const ShadContextMenuItem.inset(child: Text('Edit...')), divider, const ShadContextMenuItem.inset(child: Text('Add Profile...')), ], child: const Text('Profiles')), ], ); } } ``` --- # Popover Displays rich content in a portal, triggered by a button. ```dart import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; class PopoverPage extends StatefulWidget { const PopoverPage({super.key}); @override State createState() => _PopoverPageState(); } class _PopoverPageState extends State { final popoverController = ShadPopoverController(); final List<({String name, String initialValue})> layer = [ (name: 'Width', initialValue: '100%'), (name: 'Max. width', initialValue: '300px'), (name: 'Height', initialValue: '25px'), (name: 'Max. height', initialValue: 'none'), ]; @override void dispose() { popoverController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final textTheme = ShadTheme.of(context).textTheme; return Scaffold( body: Center( child: ShadPopover( controller: popoverController, popover: (context) => SizedBox( width: 288, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Dimensions', style: textTheme.h4, ), Text( 'Set the dimensions for the layer.', style: textTheme.p, ), const SizedBox(height: 4), ...layer .map( (e) => Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Text( e.name, textAlign: TextAlign.start, )), Expanded( flex: 2, child: ShadInput( initialValue: e.initialValue, ), ) ], ), ) .separatedBy(const SizedBox(height: 8)), ], ), ), child: ShadButton.outline( onPressed: popoverController.toggle, child: const Text('Open popover'), ), ), ), ); } } ``` --- # Progress Displays an indicator showing the completion progress of a task, typically displayed as a progress bar. ## Determinate ```dart ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width * 0.6, ), child: const ShadProgress(value: 0.5), ), ``` ## Indeterminate ```dart ConstrainedBox( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width * 0.6, ), child: const ShadProgress(), ), ``` --- # RadioGroup A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time. ```dart ShadRadioGroup( items: [ ShadRadio( label: Text('Default'), value: 'default', ), ShadRadio( label: Text('Comfortable'), value: 'comfortable', ), ShadRadio( label: Text('Nothing'), value: 'nothing', ), ], ), ``` ## Form ```dart enum NotifyAbout { all, mentions, nothing; String get message { return switch (this) { all => 'All new messages', mentions => 'Direct messages and mentions', nothing => 'Nothing', }; } } ShadRadioGroupFormField( label: const Text('Notify me about'), items: NotifyAbout.values.map( (e) => ShadRadio( value: e, label: Text(e.message), ), ), validator: (v) { if (v == null) { return 'You need to select a notification type.'; } return null; }, ), ``` --- # Resizable Resizable panel groups and layouts. ```dart class BasicResizable extends StatelessWidget { const BasicResizable({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: DecoratedBox( decoration: BoxDecoration( borderRadius: theme.radius, border: Border.all( color: theme.colorScheme.border, ), ), child: ClipRRect( borderRadius: theme.radius, child: ShadResizablePanelGroup( children: [ ShadResizablePanel( id: 0, defaultSize: .5, minSize: .2, maxSize: .8, child: Center( child: Text('One', style: theme.textTheme.large), ), ), ShadResizablePanel( id: 1, defaultSize: .5, child: ShadResizablePanelGroup( axis: Axis.vertical, children: [ ShadResizablePanel( id: 0, defaultSize: .3, child: Center( child: Text('Two', style: theme.textTheme.large)), ), ShadResizablePanel( id: 1, defaultSize: .7, child: Align( child: Text('Three', style: theme.textTheme.large)), ), ], ), ), ], ), ), ), ); } } ``` :::tip Try resizing a panel, then double-click on the handle to reset to the default size. ::: ## Vertical Use the `axis` property to change the direction of the resizable panels. ```dart class VerticalResizable extends StatelessWidget { const VerticalResizable({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: DecoratedBox( decoration: BoxDecoration( borderRadius: theme.radius, border: Border.all( color: theme.colorScheme.border, ), ), child: ClipRRect( borderRadius: theme.radius, child: ShadResizablePanelGroup( axis: Axis.vertical, children: [ ShadResizablePanel( id: 0, defaultSize: 0.3, minSize: 0.1, child: Center( child: Text('Header', style: theme.textTheme.large), ), ), ShadResizablePanel( id: 1, defaultSize: 0.7, minSize: 0.1, child: Center( child: Text('Footer', style: theme.textTheme.large), ), ), ], ), ), ), ); } } ``` ## Handle You can show the handle by using the `showHandle` property. You can customize it using the `handleIcon` or `handleIconSrc` properties. ```dart class HandleResizable extends StatelessWidget { const HandleResizable({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: DecoratedBox( decoration: BoxDecoration( borderRadius: theme.radius, border: Border.all( color: theme.colorScheme.border, ), ), child: ClipRRect( borderRadius: theme.radius, child: ShadResizablePanelGroup( showHandle: true, children: [ ShadResizablePanel( id: 0, defaultSize: .5, minSize: .2, child: Center( child: Text('Sidebar', style: theme.textTheme.large), ), ), ShadResizablePanel( id: 1, defaultSize: .5, minSize: .2, child: Center( child: Text('Content', style: theme.textTheme.large), ), ), ], ), ), ), ); } } ``` --- # Select Displays a list of options for the user to pick from—triggered by a button. ```dart final fruits = { 'apple': 'Apple', 'banana': 'Banana', 'blueberry': 'Blueberry', 'grapes': 'Grapes', 'pineapple': 'Pineapple', }; class SelectExample extends StatelessWidget { const SelectExample({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(minWidth: 180), child: ShadSelect( placeholder: const Text('Select a fruit'), options: [ Padding( padding: const EdgeInsets.fromLTRB(32, 6, 6, 6), child: Text( 'Fruits', style: theme.textTheme.muted.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.popoverForeground, ), textAlign: TextAlign.start, ), ), ...fruits.entries .map((e) => ShadOption(value: e.key, child: Text(e.value))), ], selectedOptionBuilder: (context, value) => Text(fruits[value]!), onChanged: print, ), ); } } ``` ## Scrollable ```dart final timezones = { 'North America': { 'est': 'Eastern Standard Time (EST)', 'cst': 'Central Standard Time (CST)', 'mst': 'Mountain Standard Time (MST)', 'pst': 'Pacific Standard Time (PST)', 'akst': 'Alaska Standard Time (AKST)', 'hst': 'Hawaii Standard Time (HST)', }, 'Europe & Africa': { 'gmt': 'Greenwich Mean Time (GMT)', 'cet': 'Central European Time (CET)', 'eet': 'Eastern European Time (EET)', 'west': 'Western European Summer Time (WEST)', 'cat': 'Central Africa Time (CAT)', 'eat': 'Eastern Africa Time (EAT)', }, 'Asia': { 'msk': 'Moscow Time (MSK)', 'ist': 'India Standard Time (IST)', 'cst_china': 'China Standard Time (CST)', 'jst': 'Japan Standard Time (JST)', 'kst': 'Korea Standard Time (KST)', 'ist_indonasia': 'Indonesia Standard Time (IST)', }, 'Australia & Pacific': { 'awst': 'Australian Western Standard Time (AWST)', 'acst': 'Australian Central Standard Time (ACST)', 'aest': 'Australian Eastern Standard Time (AEST)', 'nzst': 'New Zealand Standard Time (NZST)', 'fjt': 'Fiji Time (FJT)', }, 'South America': { 'art': 'Argentina Time (ART)', 'bot': 'Bolivia Time (BOT)', 'brt': 'Brasilia Time (BRT)', 'clt': 'Chile Standard Time (CLT)', }, }; List getTimezonesWidgets(ShadThemeData theme) { final widgets = []; for (final zone in timezones.entries) { widgets.add( Padding( padding: const EdgeInsets.fromLTRB(32, 6, 6, 6), child: Text( zone.key, style: theme.textTheme.muted.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.popoverForeground, ), textAlign: TextAlign.start, ), ), ); widgets.addAll(zone.value.entries .map((e) => ShadOption(value: e.key, child: Text(e.value)))); } return widgets; } class SelectExample extends StatelessWidget { const SelectExample({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ConstrainedBox( constraints: const BoxConstraints(minWidth: 280), child: ShadSelect( placeholder: const Text('Select a timezone'), options: getTimezonesWidgets(theme), selectedOptionBuilder: (context, value) { final timezone = timezones.entries .firstWhere((element) => element.value.containsKey(value)) .value[value]; return Text(timezone!); }, ), ); } } ``` ## Form ```dart final verifiedEmails = [ 'm@example.com', 'm@google.com', 'm@support.com', ]; class SelectFormField extends StatelessWidget { const SelectFormField({super.key}); @override Widget build(BuildContext context) { return ShadSelectFormField( id: 'email', minWidth: 350, initialValue: null, options: verifiedEmails .map((email) => ShadOption(value: email, child: Text(email))) .toList(), selectedOptionBuilder: (context, value) => value == 'none' ? const Text('Select a verified email to display') : Text(value), placeholder: const Text('Select a verified email to display'), validator: (v) { if (v == null) { return 'Please select an email to display'; } return null; }, ); } } ``` ## With Search ```dart const frameworks = { 'nextjs': 'Next.js', 'svelte': 'SvelteKit', 'nuxtjs': 'Nuxt.js', 'remix': 'Remix', 'astro': 'Astro', }; class SelectWithSearch extends StatefulWidget { const SelectWithSearch({super.key}); @override State createState() => _SelectWithSearchState(); } class _SelectWithSearchState extends State { var searchValue = ''; Map get filteredFrameworks => { for (final framework in frameworks.entries) if (framework.value.toLowerCase().contains(searchValue.toLowerCase())) framework.key: framework.value }; @override Widget build(BuildContext context) { return ShadSelect.withSearch( minWidth: 180, maxWidth: 300, placeholder: const Text('Select framework...'), onSearchChanged: (value) => setState(() => searchValue = value), searchPlaceholder: const Text('Search framework'), options: [ if (filteredFrameworks.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Text('No framework found'), ), ...frameworks.entries.map( (framework) { // this offstage is used to avoid the focus loss when the search results appear again // because it keeps the widget in the tree. return Offstage( offstage: !filteredFrameworks.containsKey(framework.key), child: ShadOption( value: framework.key, child: Text(framework.value), ), ); }, ) ], selectedOptionBuilder: (context, value) => Text(frameworks[value]!), ); } } ``` :::tip If you want to be able to deselect an option, you can use the `allowDeselection` property. ::: ## Multiple This example shows how to select multiple options. In addition, the `allowDeselection` property is set to `true` to allow the user to deselect an option and the `closeOnSelect` property is set to `false` to keep the popover open after selecting an option. If you tap outside the popover, it will close. ```dart final fruits = { 'apple': 'Apple', 'banana': 'Banana', 'blueberry': 'Blueberry', 'grapes': 'Grapes', 'pineapple': 'Pineapple', }; class SelectMultiple extends StatelessWidget { const SelectMultiple({super.key}); @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ShadSelect.multiple( minWidth: 340, onChanged: print, allowDeselection: true, closeOnSelect: false, placeholder: const Text('Select multiple fruits'), options: [ Padding( padding: const EdgeInsets.fromLTRB(32, 6, 6, 6), child: Text( 'Fruits', style: theme.textTheme.large, textAlign: TextAlign.start, ), ), ...fruits.entries.map( (e) => ShadOption( value: e.key, child: Text(e.value), ), ), ], selectedOptionsBuilder: (context, values) => Text(values.map((v) => v.capitalize()).join(', ')), ); } } ``` --- # Separator Visually or semantically separates content. ```dart const ShadSeparator.horizontal( thickness: 4, margin: EdgeInsets.symmetric(horizontal: 20), radius: BorderRadius.all(Radius.circular(4)), ) ``` ## Destructive ```dart const ShadSeparator.vertical( thickness: 4, margin: EdgeInsets.symmetric(vertical: 20), radius: BorderRadius.all(Radius.circular(4)), ) ``` --- # Sheet Extends the Dialog component to display content that complements the main content of the screen. ```dart ShadButton.outline( child: const Text('Open'), onPressed: () => showShadSheet( side: ShadSheetSide.right, context: context, builder: (context) => const EditProfileSheet(), ), ), final profile = [ (title: 'Name', value: 'Alexandru'), (title: 'Username', value: 'nank1ro'), ]; class EditProfileSheet extends StatelessWidget { const EditProfileSheet({super.key, required this.side}); final ShadSheetSide side; @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return ShadSheet( constraints: side == ShadSheetSide.left || side == ShadSheetSide.right ? const BoxConstraints(maxWidth: 512) : null, title: const Text('Edit Profile'), description: const Text( "Make changes to your profile here. Click save when you're done"), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 16, children: profile .map( (p) => Row( children: [ Expanded( child: Text( p.title, textAlign: TextAlign.end, style: theme.textTheme.small, ), ), const SizedBox(width: 16), Expanded( flex: 5, child: ShadInput(initialValue: p.value), ), ], ), ) .toList(), ), ), actions: const [ ShadButton(child: Text('Save changes')), ], ); } } ``` ## Side Use the `side` property to `showShadSheet` to indicate the edge of the screen where the component will appear. The values can be `top`, `right`, `bottom` or `left`. ```dart Row( mainAxisSize: MainAxisSize.min, spacing: 16, children: [ Column( spacing: 16, mainAxisSize: MainAxisSize.min, children: [ ShadButton.outline( width: 100, child: const Text('Top'), onPressed: () => showShadSheet( side: ShadSheetSide.top, context: context, builder: (context) => const EditProfileSheet(side: ShadSheetSide.top), ), ), ShadButton.outline( width: 100, child: const Text('Bottom'), onPressed: () => showShadSheet( side: ShadSheetSide.bottom, context: context, builder: (context) => const EditProfileSheet( side: ShadSheetSide.bottom), ), ), ], ), Column( spacing: 16, mainAxisSize: MainAxisSize.min, children: [ ShadButton.outline( width: 100, child: const Text('Right'), onPressed: () => showShadSheet( side: ShadSheetSide.right, context: context, builder: (context) => const EditProfileSheet( side: ShadSheetSide.right), ), ), ShadButton.outline( width: 100, child: const Text('Left'), onPressed: () => showShadSheet( side: ShadSheetSide.left, context: context, builder: (context) => const EditProfileSheet( side: ShadSheetSide.left), ), ), ], ), ], ), // See EditProfileSheet code in the previous code example ``` --- # Slider An input where the user selects a value from within a given range. ```dart ShadSlider( initialValue: 33, max: 100, ), ``` --- # Sonner An opinionated toast component. ```dart ShadButton.outline( child: const Text('Show Toast'), onPressed: () { final sonner = ShadSonner.of(context); final id = Random().nextInt(1000); final now = DateTime.now(); sonner.show( ShadToast( id: id, title: const Text('Event has been created'), description: Text(DateFormat.yMd().add_jms().format(now)), action: ShadButton( child: const Text('Undo'), onPressed: () => sonner.hide(id), ), ), ); }, ), ``` --- # Switch A control that allows the user to toggle between checked and not checked. ```dart class SwitchExample extends StatefulWidget { const SwitchExample({super.key}); @override State createState() => _SwitchExampleState(); } class _SwitchExampleState extends State { bool value = false; @override Widget build(BuildContext context) { return ShadSwitch( value: value, onChanged: (v) => setState(() => value = v), label: const Text('Airplane Mode'), ); } } ``` ## Form ```dart ShadSwitchFormField( id: 'terms', initialValue: false, inputLabel: const Text('I accept the terms and conditions'), onChanged: (v) {}, inputSublabel: const Text('You agree to our Terms and Conditions'), validator: (v) { if (!v) { return 'You must accept the terms and conditions'; } return null; }, ) ``` --- # Table A responsive table component. ## List Use the `ShadTable.list` widget to create a table from a two dimensional array of children. Use it just for **small** tables, because every child will be created. ```dart import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; const invoices = [ ( invoice: "INV001", paymentStatus: "Paid", totalAmount: r"$250.00", paymentMethod: "Credit Card", ), ( invoice: "INV002", paymentStatus: "Pending", totalAmount: r"$150.00", paymentMethod: "PayPal", ), ( invoice: "INV003", paymentStatus: "Unpaid", totalAmount: r"$350.00", paymentMethod: "Bank Transfer", ), ( invoice: "INV004", paymentStatus: "Paid", totalAmount: r"$450.00", paymentMethod: "Credit Card", ), ( invoice: "INV005", paymentStatus: "Paid", totalAmount: r"$550.00", paymentMethod: "PayPal", ), ( invoice: "INV006", paymentStatus: "Pending", totalAmount: r"$200.00", paymentMethod: "Bank Transfer", ), ( invoice: "INV007", paymentStatus: "Unpaid", totalAmount: r"$300.00", paymentMethod: "Credit Card", ), ]; class TablePage extends StatelessWidget { const TablePage({ super.key, }); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 600, // added just to center the table vertically maxHeight: 450, ), child: ShadTable.list( header: const [ ShadTableCell.header(child: Text('Invoice')), ShadTableCell.header(child: Text('Status')), ShadTableCell.header(child: Text('Method')), ShadTableCell.header( alignment: Alignment.centerRight, child: Text('Amount'), ), ], footer: const [ ShadTableCell.footer(child: Text('Total')), ShadTableCell.footer(child: Text('')), ShadTableCell.footer(child: Text('')), ShadTableCell.footer( alignment: Alignment.centerRight, child: Text(r'$2500.00'), ), ], columnSpanExtent: (index) { if (index == 2) return const FixedTableSpanExtent(130); if (index == 3) { return const MaxTableSpanExtent( FixedTableSpanExtent(120), RemainingTableSpanExtent(), ); } // uses the default value return null; }, children: invoices .map( (invoice) => [ ShadTableCell( child: Text( invoice.invoice, style: const TextStyle( fontWeight: FontWeight.w500, ), ), ), ShadTableCell(child: Text(invoice.paymentStatus)), ShadTableCell(child: Text(invoice.paymentMethod)), ShadTableCell( alignment: Alignment.centerRight, child: Text( invoice.totalAmount, ), ), ], ), ), ), ), ); } } ``` ## Builder You can also use a builder to create the table. This method is preferred for **large** tables because widgets are created on demand. Here it is the same table as above, but using a builder. ```dart const invoices = [ [ "INV001", "Paid", "Credit Card", r"$250.00", ], [ "INV002", "Pending", "PayPal", r"$150.00", ], [ "INV003", "Unpaid", "Bank Transfer", r"$350.00", ], [ "INV004", "Paid", "Credit Card", r"$450.00", ], [ "INV005", "Paid", "PayPal", r"$550.00", ], [ "INV006", "Pending", "Bank Transfer", r"$200.00", ], [ "INV007", "Unpaid", "Credit Card", r"$300.00", ], ]; final headings = [ 'Invoice', 'Status', 'Method', 'Amount', ]; class TableExample extends StatelessWidget { const TableExample({super.key}); @override Widget build(BuildContext context) { return ShadTable( columnCount: invoices[0].length, rowCount: invoices.length, header: (context, column) { final isLast = column == headings.length - 1; return ShadTableCell.header( alignment: isLast ? Alignment.centerRight : null, child: Text(headings[column]), ); }, columnSpanExtent: (index) { if (index == 2) return const FixedTableSpanExtent(150); if (index == 3) { return const MaxTableSpanExtent( FixedTableSpanExtent(120), RemainingTableSpanExtent(), ); } return null; }, builder: (context, index) { final invoice = invoices[index.row]; return ShadTableCell( alignment: index.column == invoice.length - 1 ? Alignment.centerRight : Alignment.centerLeft, child: Text( invoice[index.column], style: index.column == 0 ? const TextStyle(fontWeight: FontWeight.w500) : null, ), ); }, footer: (context, column) { if (column == 0) { return const ShadTableCell.footer( child: Text( 'Total', style: TextStyle(fontWeight: FontWeight.w500), ), ); } if (column == 3) { return const ShadTableCell.footer( alignment: Alignment.centerRight, child: Text( r'$2500.00', ), ); } return const ShadTableCell(child: SizedBox()); }, ); } } ``` --- # Tabs A set of layered sections of content—known as tab panels—that are displayed one at a time. ```dart class TabsExample extends StatelessWidget { const TabsExample({super.key}); @override Widget build(BuildContext context) { return ShadTabs( value: 'account', tabBarConstraints: const BoxConstraints(maxWidth: 400), contentConstraints: const BoxConstraints(maxWidth: 400), tabs: [ ShadTab( value: 'account', content: ShadCard( title: const Text('Account'), description: const Text( "Make changes to your account here. Click save when you're done."), footer: const ShadButton(child: Text('Save changes')), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 16), ShadInputFormField( label: const Text('Name'), initialValue: 'Ale', ), const SizedBox(height: 8), ShadInputFormField( label: const Text('Username'), initialValue: 'nank1ro', ), const SizedBox(height: 16), ], ), ), child: const Text('Account'), ), ShadTab( value: 'password', content: ShadCard( title: const Text('Password'), description: const Text( "Change your password here. After saving, you'll be logged out."), footer: const ShadButton(child: Text('Save password')), child: Column( children: [ const SizedBox(height: 16), ShadInputFormField( label: const Text('Current password'), obscureText: true, ), const SizedBox(height: 8), ShadInputFormField( label: const Text('New password'), obscureText: true, ), const SizedBox(height: 16), ], ), ), child: const Text('Password'), ), ], ); } } ``` --- # Textarea Displays a form textarea or a component that looks like a textarea. ```dart ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: const ShadTextarea( placeholder: Text('Type your message here'), ), ), ``` ## Form ```dart ShadTextareaFormField( id: 'bio', label: const Text('Bio'), placeholder: const Text('Tell us a little bit about yourself'), description: const Text( 'You can @mention other users and organizations.'), validator: (v) { if (v.length < 10) { return 'Bio must be at least 10 characters.'; } return null; }, ) ``` --- # Time Picker A time picker component. ```dart class PrimaryTimePicker extends StatelessWidget { const PrimaryTimePicker({super.key}); @override Widget build(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: const ShadTimePicker( trailing: Padding( padding: EdgeInsets.only(left: 8, top: 14), child: Icon(LucideIcons.clock4), ), ), ); } } ``` ## Form ```dart ShadTimePickerFormField( label: const Text('Pick a time'), onChanged: print, description: const Text('The time of the day you want to pick'), validator: (v) => v == null ? 'A time is required' : null, ) ``` ## ShadTimePickerFormField.period ```dart ShadTimePickerFormField.period( label: const Text('Pick a time'), onChanged: print, description: const Text('The time of the day you want to pick'), validator: (v) => v == null ? 'A time is required' : null, ), ``` --- # Toast A succinct message that is displayed temporarily. ```dart ShadButton.outline( child: const Text('Add to calendar'), onPressed: () { ShadToaster.of(context).show( ShadToast( title: const Text('Scheduled: Catch up'), description: const Text('Friday, February 10, 2023 at 5:57 PM'), action: ShadButton.outline( child: const Text('Undo'), onPressed: () => ShadToaster.of(context).hide(), ), ), ); }, ), ``` ## Simple ```dart ShadButton.outline( child: const Text('Show Toast'), onPressed: () { ShadToaster.of(context).show( const ShadToast( description: Text('Your message has been sent.'), ), ); }, ), ``` ## With Title ```dart ShadButton.outline( child: const Text('Show Toast'), onPressed: () { ShadToaster.of(context).show( const ShadToast( title: Text('Uh oh! Something went wrong'), description: Text('There was a problem with your request'), ), ); }, ), ``` ## With Action ```dart ShadButton.outline( child: const Text('Show Toast'), onPressed: () { ShadToaster.of(context).show( ShadToast( title: const Text('Uh oh! Something went wrong'), description: const Text('There was a problem with your request'), action: ShadButton.outline( child: const Text('Try again'), onPressed: () => ShadToaster.of(context).hide(), ), ), ); }, ), ``` ## Destructive ```dart final theme = ShadTheme.of(context); ShadButton.outline( child: const Text('Show Toast'), onPressed: () { ShadToaster.of(context).show( ShadToast.destructive( title: const Text('Uh oh! Something went wrong'), description: const Text('There was a problem with your request'), action: ShadButton.destructive( child: const Text('Try again'), decoration: ShadDecoration( border: ShadBorder.all( color: theme.colorScheme.destructiveForeground, width: 1, ), ), onPressed: () => ShadToaster.of(context).hide(), ), ), ); }, ), ``` --- # Tooltip A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. ```dart ShadTooltip( builder: (context) => const Text('Add to library'), child: ShadButton.outline( child: const Text('Hover/Focus'), onPressed: () {}, ), ), ``` :::caution The tooltip works on hover only if the child uses a `ShadGestureDetector`. If you don't use a `ShadButton` or something similar that implements `ShadGestureDetector` hover will not work. If, for example, you want to just show an image as child, wrap it with `ShadGestureDetector` to make it working. ::: --- # Decorator Decorates most of the components of the library using a `ShadDecoration` handled by the `ShadDecorator` component. ## Default ```dart ShadDecoration( secondaryBorder: ShadBorder.all( padding: const EdgeInsets.all(4), width: 0, ), secondaryFocusedBorder: ShadBorder.all( width: 2, color: colorScheme.ring, radius: radius.add(radius / 2), padding: const EdgeInsets.all(2), ), labelStyle: textTheme.muted.copyWith( fontWeight: FontWeight.w500, color: colorScheme.foreground, ), errorStyle: textTheme.muted.copyWith( fontWeight: FontWeight.w500, color: colorScheme.destructive, ), labelPadding: const EdgeInsets.only(bottom: 8), descriptionStyle: textTheme.muted, descriptionPadding: const EdgeInsets.only(top: 8), errorPadding: const EdgeInsets.only(top: 8), errorLabelStyle: textTheme.muted.copyWith( fontWeight: FontWeight.w500, color: colorScheme.destructive, ), ); ``` ## Secondary Border By default, a secondary border is drawn around the focusable components. If you want to disable it and instead make bolder the primary border, you just need to add the `disableSecondaryBorder` property to the theme. ```dart ShadThemeData( // Disables the secondary border disableSecondaryBorder: true, ), ``` :::caution Be aware, this change is not recommended, as it may lead to accessibility issues. The secondary border is there to help users understand which component is focused. ::: --- # Responsive In *shadcn_ui* the responsiveness is an important part of the library. The `ShadTheme` supports a customizable set of breakpoints. ## Default ```dart ShadThemeData( breakpoints: ShadBreakpoints( tn: 0, // tiny sm: 640, // small md: 768, // medium lg: 1024, // large xl: 1280, // extra large xxl: 1536, // extra extra large ), ); ``` ## Current breakpoint To get the current breakpoint you can use `ShadResponsiveBuilder` or `context.breakpoint`, eg: ```dart ShadResponsiveBuilder( builder: (context, breakpoint) { final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm; ... }, ), ``` which is equivalent to: ```dart final sm = context.breakpoint >= ShadTheme.of(context).breakpoints.sm; ``` In Tailwind CSS, it's common to say that *sm* is not for small screens, but will target also the largest sizes if you don't provide a larger breakpoint. That's why I'm using the `>=` operator. If you just want to check if you're in a specific breakpoint, use the `==` operator. ## Sealed class The breakpoint returned is a sealed class so you can switch any size. ```dart ShadResponsiveBuilder( builder: (context, breakpoint) { return switch (breakpoint) { ShadBreakpointTN() => const Text('Tiny'), ShadBreakpointSM() => const Text('Small'), ShadBreakpointMD() => const Text('Medium'), ShadBreakpointLG() => const Text('Large'), ShadBreakpointXL() => const Text('Extra Large'), ShadBreakpointXXL() => const Text('Extra Extra Large'), }; }, ), ```