diff --git a/docs/6.x/docs/components/Toolbar/Toolbar.mdx b/docs/6.x/docs/components/Toolbar/Toolbar.mdx
new file mode 100644
index 0000000000..9f58039b48
--- /dev/null
+++ b/docs/6.x/docs/components/Toolbar/Toolbar.mdx
@@ -0,0 +1,163 @@
+---
+title: Toolbar
+---
+
+import PropTable from '@docs/components/PropTable.tsx';
+import ExtendsLink from '@docs/components/ExtendsLink.tsx';
+import ThemeColorsTable from '@docs/components/ThemeColorsTable.tsx';
+import ScreenshotTabs from '@docs/components/ScreenshotTabs.tsx';
+import ExtendedExample from '@docs/components/ExtendedExample.tsx';
+
+A toolbar groups icon actions behind a shared surface.
+
+It comes in two `variant`s: `floating`, a self-positioned pill anchored wherever you place it
+(similar to a `FAB`), and `docked`, a full-width bar pinned to the bottom edge that extends
+into the safe-area insets automatically. A floating toolbar can also be laid out vertically
+via `orientation`.
+
+The `colorScheme` prop controls how contained `IconButton`/`Button` children are colored. By
+default: `standard` keeps them neutral against a surface-colored container, while `vibrant`
+gives the toolbar itself a bold, primary-tinted container and switches selected/unselected
+children to matching vibrant colors, making the toolbar stand out as a focal point on the
+screen.
+
+
+
+
+
+
+
+## Usage
+```js
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Toolbar, IconButton } from 'react-native-paper';
+
+const MyComponent = () => (
+
+
+ {}} />
+ {}} />
+ {}} />
+
+
+);
+
+const styles = StyleSheet.create({
+ anchor: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 24,
+ alignItems: 'center',
+ },
+});
+
+export default MyComponent;
+```
+
+
+ ## Props
+
+
+
+
+
+
+### children (required)
+
+
+
+
+
+
+
+### variant
+
+
+
+
+
+
+
+### orientation
+
+
+
+
+
+
+
+### colorScheme
+
+
+
+
+
+
+
+### containerColor
+
+
+
+
+
+
+
+### style
+
+
+
+
+
+
+
+### contentContainerStyle
+
+
+
+
+
+
+
+### testID
+
+
+
+
+
+
+
+### aria-label
+
+
+
+
+
+
+
+### theme
+
+
+
+
+
+
+
+### ref
+
+
+
+
+
+
+
+
+
+
+ ## Theme colors
+
+
+
+
+
diff --git a/docs/6.x/docs/components/Toolbar/_meta.json b/docs/6.x/docs/components/Toolbar/_meta.json
new file mode 100644
index 0000000000..26b5bf88f7
--- /dev/null
+++ b/docs/6.x/docs/components/Toolbar/_meta.json
@@ -0,0 +1,3 @@
+[
+ "Toolbar"
+]
diff --git a/docs/6.x/docs/components/_meta.json b/docs/6.x/docs/components/_meta.json
index 6f793d41a3..b272f66237 100644
--- a/docs/6.x/docs/components/_meta.json
+++ b/docs/6.x/docs/components/_meta.json
@@ -149,6 +149,13 @@
"collapsible": true,
"collapsed": false
},
+ {
+ "type": "dir",
+ "name": "Toolbar",
+ "label": "Toolbar",
+ "collapsible": true,
+ "collapsed": false
+ },
{
"type": "dir",
"name": "Tooltip",
diff --git a/docs/component-docs.config.ts b/docs/component-docs.config.ts
index bad25d4eec..0bf5518f60 100644
--- a/docs/component-docs.config.ts
+++ b/docs/component-docs.config.ts
@@ -133,6 +133,9 @@ const pages = {
ToggleButtonGroup: 'ToggleButton/ToggleButtonGroup',
ToggleButtonRow: 'ToggleButton/ToggleButtonRow',
},
+ Toolbar: {
+ Toolbar: 'Toolbar/Toolbar',
+ },
Tooltip: {
Tooltip: 'Tooltip/Tooltip',
},
diff --git a/docs/public/screenshots/toolbar_docked_standard.png b/docs/public/screenshots/toolbar_docked_standard.png
new file mode 100644
index 0000000000..e4b4b677a5
Binary files /dev/null and b/docs/public/screenshots/toolbar_docked_standard.png differ
diff --git a/docs/public/screenshots/toolbar_docked_vibrant.png b/docs/public/screenshots/toolbar_docked_vibrant.png
new file mode 100644
index 0000000000..cefdd29492
Binary files /dev/null and b/docs/public/screenshots/toolbar_docked_vibrant.png differ
diff --git a/docs/public/screenshots/toolbar_floating_horizontal_standard.png b/docs/public/screenshots/toolbar_floating_horizontal_standard.png
new file mode 100644
index 0000000000..c6404c8f3e
Binary files /dev/null and b/docs/public/screenshots/toolbar_floating_horizontal_standard.png differ
diff --git a/docs/public/screenshots/toolbar_floating_horizontal_vibrant.png b/docs/public/screenshots/toolbar_floating_horizontal_vibrant.png
new file mode 100644
index 0000000000..5e9458bbb9
Binary files /dev/null and b/docs/public/screenshots/toolbar_floating_horizontal_vibrant.png differ
diff --git a/docs/public/screenshots/toolbar_floating_vertical_standard.png b/docs/public/screenshots/toolbar_floating_vertical_standard.png
new file mode 100644
index 0000000000..eaf5e3b11b
Binary files /dev/null and b/docs/public/screenshots/toolbar_floating_vertical_standard.png differ
diff --git a/docs/public/screenshots/toolbar_floating_vertical_vibrant.png b/docs/public/screenshots/toolbar_floating_vertical_vibrant.png
new file mode 100644
index 0000000000..e0a18bac97
Binary files /dev/null and b/docs/public/screenshots/toolbar_floating_vertical_vibrant.png differ
diff --git a/docs/scripts/generate-rspress-content.ts b/docs/scripts/generate-rspress-content.ts
index 1c9accd93a..2e273307d6 100644
--- a/docs/scripts/generate-rspress-content.ts
+++ b/docs/scripts/generate-rspress-content.ts
@@ -128,8 +128,20 @@ const getVersionComponentOrder = (version: string): MetaEntry[] => {
return fromConfig;
}
+ // `componentDocsConfig.pages` is shared across versions, but components
+ // added there for 6.x-only features (e.g. `Toolbar`) have no docs source
+ // under 5.x keep them out of the 5.x nav rather than linking to an
+ // empty/missing page.
+ const componentsDir = path.join(getVersionDocsDir(version), 'components');
+ const existsForVersion = (name: string) =>
+ fs.existsSync(path.join(componentsDir, name)) ||
+ fs.existsSync(path.join(componentsDir, `${name}.mdx`)) ||
+ fs.existsSync(path.join(componentsDir, `${name}.md`));
+
return [
- ...fromConfig,
+ ...fromConfig.filter((entry) =>
+ existsForVersion(typeof entry === 'string' ? entry : entry.name)
+ ),
{
type: 'dir',
name: 'HelperText',
diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json
index 56b356912b..63b6df4099 100644
--- a/docs/src/data/componentDocs6x.json
+++ b/docs/src/data/componentDocs6x.json
@@ -11955,6 +11955,136 @@
],
"group": "ToggleButton"
},
+ "Toolbar/Toolbar": {
+ "filepath": "Toolbar/Toolbar.tsx",
+ "title": "Toolbar",
+ "description": "A toolbar groups icon actions behind a shared surface.\n\nIt comes in two `variant`s: `floating`, a self-positioned pill anchored wherever you place it\n(similar to a `FAB`), and `docked`, a full-width bar pinned to the bottom edge that extends\ninto the safe-area insets automatically. A floating toolbar can also be laid out vertically\nvia `orientation`.\n\nThe `colorScheme` prop controls how contained `IconButton`/`Button` children are colored. By\ndefault: `standard` keeps them neutral against a surface-colored container, while `vibrant`\ngives the toolbar itself a bold, primary-tinted container and switches selected/unselected\nchildren to matching vibrant colors, making the toolbar stand out as a focal point on the\nscreen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport { Toolbar, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n {}} />\n {}} />\n {}} />\n \n \n);\n\nconst styles = StyleSheet.create({\n anchor: {\n position: 'absolute',\n left: 0,\n right: 0,\n bottom: 24,\n alignItems: 'center',\n },\n});\n\nexport default MyComponent;\n```",
+ "link": "toolbar",
+ "data": {
+ "description": "A toolbar groups icon actions behind a shared surface.\n\nIt comes in two `variant`s: `floating`, a self-positioned pill anchored wherever you place it\n(similar to a `FAB`), and `docked`, a full-width bar pinned to the bottom edge that extends\ninto the safe-area insets automatically. A floating toolbar can also be laid out vertically\nvia `orientation`.\n\nThe `colorScheme` prop controls how contained `IconButton`/`Button` children are colored. By\ndefault: `standard` keeps them neutral against a surface-colored container, while `vibrant`\ngives the toolbar itself a bold, primary-tinted container and switches selected/unselected\nchildren to matching vibrant colors, making the toolbar stand out as a focal point on the\nscreen.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport { Toolbar, IconButton } from 'react-native-paper';\n\nconst MyComponent = () => (\n \n \n {}} />\n {}} />\n {}} />\n \n \n);\n\nconst styles = StyleSheet.create({\n anchor: {\n position: 'absolute',\n left: 0,\n right: 0,\n bottom: 24,\n alignItems: 'center',\n },\n});\n\nexport default MyComponent;\n```",
+ "displayName": "Toolbar",
+ "methods": [],
+ "statics": [],
+ "props": {
+ "children": {
+ "required": true,
+ "tsType": {
+ "name": "ReactReactNode",
+ "raw": "React.ReactNode"
+ },
+ "description": "Content of the toolbar, typically a row of `IconButton`s."
+ },
+ "variant": {
+ "required": false,
+ "tsType": {
+ "name": "Variant"
+ },
+ "description": "`floating` is a self-positioned pill (like a `FAB`); `docked` is a\nfull-width bar anchored to the bottom edge, extending into safe-area\ninsets automatically. Defaults to `floating`.",
+ "defaultValue": {
+ "value": "'floating'",
+ "computed": false
+ }
+ },
+ "orientation": {
+ "required": false,
+ "tsType": {
+ "name": "Orientation"
+ },
+ "description": "Layout axis for `floating` (`docked` is always horizontal, per spec).\nDefaults to `horizontal`.",
+ "defaultValue": {
+ "value": "'horizontal'",
+ "computed": false
+ }
+ },
+ "colorScheme": {
+ "required": false,
+ "tsType": {
+ "name": "ColorScheme"
+ },
+ "description": "Role-color preset. Sets default colors on direct, mode-less `IconButton`/`Button`\nchildren, unless they already set their own. Defaults to `standard`.",
+ "defaultValue": {
+ "value": "'standard'",
+ "computed": false
+ }
+ },
+ "containerColor": {
+ "required": false,
+ "tsType": {
+ "name": "ColorValue"
+ },
+ "description": "Override the container (background) color."
+ },
+ "style": {
+ "required": false,
+ "tsType": {
+ "name": "StyleProp",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ }
+ ],
+ "raw": "StyleProp"
+ },
+ "description": "Style for positioning `floating`'s pill, or overriding `docked`'s\ndefault anchoring."
+ },
+ "contentContainerStyle": {
+ "required": false,
+ "tsType": {
+ "name": "StyleProp",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ }
+ ],
+ "raw": "StyleProp"
+ },
+ "description": "Style for the row/column wrapping `children`. Overrides the default\npadding/gap."
+ },
+ "testID": {
+ "required": false,
+ "tsType": {
+ "name": "string"
+ },
+ "description": "TestID used for testing purposes.",
+ "defaultValue": {
+ "value": "'toolbar'",
+ "computed": false
+ }
+ },
+ "aria-label": {
+ "required": false,
+ "tsType": {
+ "name": "string"
+ },
+ "description": "Accessibility label for the toolbar group. `children` still need\ntheir own `aria-label`s."
+ },
+ "theme": {
+ "required": false,
+ "tsType": {
+ "name": "ThemeProp"
+ },
+ "description": ""
+ },
+ "ref": {
+ "required": false,
+ "tsType": {
+ "name": "ReactRefObject",
+ "raw": "React.RefObject",
+ "elements": [
+ {
+ "name": "View"
+ }
+ ]
+ },
+ "description": ""
+ }
+ }
+ },
+ "type": "component",
+ "dependencies": [
+ "src/components/Toolbar/Toolbar.tsx"
+ ]
+ },
"Tooltip/Tooltip": {
"filepath": "Tooltip/Tooltip.tsx",
"title": "Tooltip",
diff --git a/docs/src/data/screenshots.ts b/docs/src/data/screenshots.ts
index 92bf8f2788..a5afa8ffe3 100644
--- a/docs/src/data/screenshots.ts
+++ b/docs/src/data/screenshots.ts
@@ -152,6 +152,17 @@ export const screenshots = {
ToggleButton: 'screenshots/toggle-button.png',
'ToggleButton.Group': 'screenshots/toggle-button-group.gif',
'ToggleButton.Row': 'screenshots/toggle-button-row.gif',
+ Toolbar: {
+ 'floating horizontal':
+ 'screenshots/toolbar_floating_horizontal_standard.png',
+ 'floating horizontal - vibrant':
+ 'screenshots/toolbar_floating_horizontal_vibrant.png',
+ 'floating vertical': 'screenshots/toolbar_floating_vertical_standard.png',
+ 'floating vertical - vibrant':
+ 'screenshots/toolbar_floating_vertical_vibrant.png',
+ docked: 'screenshots/toolbar_docked_standard.png',
+ 'docked - vibrant': 'screenshots/toolbar_docked_vibrant.png',
+ },
Tooltip: 'screenshots/tooltip.png',
TouchableRipple: 'screenshots/touchable-ripple.gif',
};
diff --git a/docs/src/data/themeColors.ts b/docs/src/data/themeColors.ts
index 20f16962f7..c895d9cb89 100644
--- a/docs/src/data/themeColors.ts
+++ b/docs/src/data/themeColors.ts
@@ -320,6 +320,33 @@ export const themeColors = {
textColor: 'theme.colors.onSurface',
},
},
+ Toolbar: {
+ standard: {
+ unselected: {
+ backgroundColor: 'theme.colors.surfaceContainer',
+ iconColor: 'theme.colors.onSurfaceVariant',
+ textColor: 'theme.colors.onSurfaceVariant',
+ },
+ selected: {
+ backgroundColor: 'theme.colors.secondaryContainer',
+ iconColor:
+ 'theme.colors.onSecondaryFixedVariant (light) / theme.colors.onSecondaryContainer (dark)',
+ },
+ },
+ vibrant: {
+ unselected: {
+ backgroundColor: 'theme.colors.primaryContainer',
+ iconColor:
+ 'theme.colors.onPrimaryFixedVariant (light) / theme.colors.onPrimaryContainer (dark)',
+ textColor:
+ 'theme.colors.onPrimaryFixedVariant (light) / theme.colors.onPrimaryContainer (dark)',
+ },
+ selected: {
+ backgroundColor: 'theme.colors.surfaceContainer',
+ iconColor: 'theme.colors.onSurface',
+ },
+ },
+ },
Tooltip: {
'-': {
backgroundColor: 'theme.colors.onSurface',
diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx
index 53132e299f..6e703a6f4a 100644
--- a/example/src/ExampleList.tsx
+++ b/example/src/ExampleList.tsx
@@ -45,6 +45,7 @@ import TextInputExample from './Examples/TextInputExample';
import ThemeExample from './Examples/ThemeExample';
import ThemingWithReactNavigation from './Examples/ThemingWithReactNavigation';
import ToggleButtonExample from './Examples/ToggleButtonExample';
+import ToolbarExample from './Examples/ToolbarExample';
import TooltipExample from './Examples/TooltipExample';
import TouchableRippleExample from './Examples/TouchableRippleExample';
@@ -84,6 +85,7 @@ export const mainExamples = {
Text: TextExample,
TextInput: TextInputExample,
ToggleButton: ToggleButtonExample,
+ Toolbar: ToolbarExample,
TooltipExample,
TouchableRipple: TouchableRippleExample,
Theme: ThemeExample,
diff --git a/example/src/Examples/ToolbarExample.tsx b/example/src/Examples/ToolbarExample.tsx
new file mode 100644
index 0000000000..abaf00eccc
--- /dev/null
+++ b/example/src/Examples/ToolbarExample.tsx
@@ -0,0 +1,239 @@
+import * as React from 'react';
+import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
+
+import {
+ Chip,
+ Divider,
+ IconButton,
+ Text,
+ Toolbar,
+ useTheme,
+} from 'react-native-paper';
+import type {
+ ToolbarColorScheme,
+ ToolbarOrientation,
+ ToolbarVariant,
+} from 'react-native-paper';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const variants: ToolbarVariant[] = ['floating', 'docked'];
+const orientations: ToolbarOrientation[] = ['horizontal', 'vertical'];
+const colorSchemes: ToolbarColorScheme[] = ['standard', 'vibrant'];
+
+const toolbarItems = [
+ { icon: 'format-bold', label: 'Bold' },
+ { icon: 'format-italic', label: 'Italic' },
+ { icon: 'format-underline', label: 'Underline' },
+] as const;
+
+// Dummy list content, purely to give the screen something to scroll behind
+// the toolbar.
+const rows = Array.from({ length: 40 }, (_, i) => ({
+ id: String(i + 1),
+ text: `Item ${i + 1}`,
+}));
+
+type ChipRowProps = {
+ label: string;
+ options: readonly T[];
+ value: T;
+ onChange: (value: T) => void;
+ // Controls that don't apply to the current variant stay visible but
+ // greyed out and non-interactive, per MD3's disabled-state guidance—
+ // hiding them outright would shift the layout and lose the user's place.
+ disabled?: boolean;
+};
+
+const ChipRow = ({
+ label,
+ options,
+ value,
+ onChange,
+ disabled = false,
+}: ChipRowProps) => (
+
+
+ {label}
+
+
+ {options.map((option) => (
+ onChange(option)}
+ >
+ {option}
+
+ ))}
+
+
+);
+
+const ToolbarExample = () => {
+ const theme = useTheme();
+ const insets = useSafeAreaInsets();
+
+ const [variant, setVariant] = React.useState('floating');
+ const [orientation, setOrientation] =
+ React.useState('horizontal');
+ const [colorScheme, setColorScheme] =
+ React.useState('standard');
+
+ const isFloating = variant === 'floating';
+ const isVertical = isFloating && orientation === 'vertical';
+
+ const renderItem = React.useCallback(
+ ({ item }: { item: (typeof rows)[number] }) => (
+
+ {item.text}
+
+ ),
+ []
+ );
+
+ const toolbarChildren = (
+ <>
+ {toolbarItems.map(({ icon, label }) => (
+ {}}
+ />
+ ))}
+ >
+ );
+
+ return (
+
+
+
+
+
+
+
+ item.id}
+ contentContainerStyle={[
+ styles.listContent,
+ !isFloating
+ ? { paddingBottom: insets.bottom + 64 }
+ : isVertical
+ ? { paddingBottom: insets.bottom + 24 }
+ : { paddingBottom: insets.bottom + 96 },
+ ]}
+ />
+ {isFloating ? (
+ // `floating` doesn't anchor itself (same as `FAB`)—position it with
+ // a wrapping `View`, same as the component's own doc example.
+
+
+ {toolbarChildren}
+
+
+ ) : (
+ // `docked` anchors itself, flush to the bottom edge, on its own.
+
+ {toolbarChildren}
+
+ )}
+
+ );
+};
+
+ToolbarExample.title = 'Toolbar';
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ horizontalAnchor: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ },
+ verticalAnchor: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ },
+ controls: {
+ paddingTop: 8,
+ paddingBottom: 8,
+ },
+ divider: {
+ marginTop: 8,
+ },
+ chipRow: {
+ paddingVertical: 4,
+ },
+ chipRowLabel: {
+ paddingHorizontal: 16,
+ paddingBottom: 6,
+ },
+ chipRowContent: {
+ paddingHorizontal: 16,
+ gap: 8,
+ },
+ disabled: {
+ opacity: 0.38,
+ },
+ list: {
+ flex: 1,
+ },
+ listContent: {
+ paddingHorizontal: 16,
+ },
+ listItem: {
+ paddingVertical: 12,
+ },
+});
+
+export default ToolbarExample;
diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx
new file mode 100644
index 0000000000..a0b5fec14f
--- /dev/null
+++ b/src/components/Toolbar/Toolbar.tsx
@@ -0,0 +1,267 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
+
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { ToolbarTokens } from './tokens';
+import type { ColorScheme, Orientation, Variant } from './tokens';
+import {
+ getSpacing,
+ resolveContainerColor,
+ resolveElevation,
+ withToolbarChildColors,
+} from './utils';
+import { useInternalTheme } from '../../core/theming';
+import { resolveCornerRadius } from '../../theme/utils/shape';
+import type { ThemeProp } from '../../types';
+import Surface from '../Surface';
+
+export type Props = {
+ /**
+ * Content of the toolbar, typically a row of `IconButton`s.
+ */
+ children: React.ReactNode;
+ /**
+ * `floating` is a self-positioned pill (like a `FAB`); `docked` is a
+ * full-width bar anchored to the bottom edge, extending into safe-area
+ * insets automatically. Defaults to `floating`.
+ */
+ variant?: Variant;
+ /**
+ * Layout axis for `floating` (`docked` is always horizontal, per spec).
+ * Defaults to `horizontal`.
+ */
+ orientation?: Orientation;
+ /**
+ * Role-color preset. Sets default colors on direct, mode-less `IconButton`/`Button`
+ * children, unless they already set their own. Defaults to `standard`.
+ */
+ colorScheme?: ColorScheme;
+ /**
+ * Override the container (background) color.
+ */
+ containerColor?: ColorValue;
+ /**
+ * Style for positioning `floating`'s pill, or overriding `docked`'s
+ * default anchoring.
+ */
+ style?: StyleProp;
+ /**
+ * Style for the row/column wrapping `children`. Overrides the default
+ * padding/gap.
+ */
+ contentContainerStyle?: StyleProp;
+ /**
+ * TestID used for testing purposes.
+ */
+ testID?: string;
+ /**
+ * Accessibility label for the toolbar group. `children` still need
+ * their own `aria-label`s.
+ */
+ 'aria-label'?: string;
+ /**
+ * @optional
+ */
+ theme?: ThemeProp;
+ ref?: React.RefObject;
+};
+
+/**
+ * A toolbar groups icon actions behind a shared surface.
+ *
+ * It comes in two `variant`s: `floating`, a self-positioned pill anchored wherever you place it
+ * (similar to a `FAB`), and `docked`, a full-width bar pinned to the bottom edge that extends
+ * into the safe-area insets automatically. A floating toolbar can also be laid out vertically
+ * via `orientation`.
+ *
+ * The `colorScheme` prop controls how contained `IconButton`/`Button` children are colored. By
+ * default: `standard` keeps them neutral against a surface-colored container, while `vibrant`
+ * gives the toolbar itself a bold, primary-tinted container and switches selected/unselected
+ * children to matching vibrant colors, making the toolbar stand out as a focal point on the
+ * screen.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { StyleSheet, View } from 'react-native';
+ * import { Toolbar, IconButton } from 'react-native-paper';
+ *
+ * const MyComponent = () => (
+ *
+ *
+ * {}} />
+ * {}} />
+ * {}} />
+ *
+ *
+ * );
+ *
+ * const styles = StyleSheet.create({
+ * anchor: {
+ * position: 'absolute',
+ * left: 0,
+ * right: 0,
+ * bottom: 24,
+ * alignItems: 'center',
+ * },
+ * });
+ *
+ * export default MyComponent;
+ * ```
+ */
+const Toolbar = ({
+ children,
+ variant = 'floating',
+ orientation = 'horizontal',
+ colorScheme = 'standard',
+ containerColor,
+ style,
+ contentContainerStyle,
+ testID = 'toolbar',
+ 'aria-label': ariaLabel,
+ theme: themeOverrides,
+ ref,
+}: Props) => {
+ const theme = useInternalTheme(themeOverrides);
+ const insets = useSafeAreaInsets();
+
+ const isDocked = variant === 'docked';
+ const isVertical = !isDocked && orientation === 'vertical';
+
+ const backgroundColor = resolveContainerColor({
+ theme,
+ colorScheme,
+ containerColor,
+ });
+ const borderRadius = resolveCornerRadius(
+ theme,
+ isDocked
+ ? ToolbarTokens.docked.containerShape
+ : ToolbarTokens.floating.containerShape
+ );
+ const elevation = resolveElevation({ isDocked });
+
+ // Cross-axis thickness is always the spec value (64dp); insets are
+ // never mixed in, so the icon band never grows/shrinks with the safe
+ // area (`docked` extends into insets separately, see
+ // `dockedInsetMargin` below).
+ const thickness = isDocked
+ ? ToolbarTokens.docked.containerHeight
+ : ToolbarTokens.floating.containerHeight;
+ const { paddingLeading, paddingTrailing, gap } = getSpacing({ variant });
+
+ // `docked`'s content row is a fixed 64dp band (see `thickness` above),
+ // so top/bottom padding would clip taller children (e.g. a `Button`
+ // label). `floating` has no fixed-height row, so it pads every side.
+ const contentPadding = isDocked
+ ? { paddingLeft: paddingLeading, paddingRight: paddingTrailing }
+ : {
+ paddingTop: paddingLeading,
+ paddingBottom: paddingLeading,
+ paddingLeft: paddingLeading,
+ paddingRight: paddingTrailing,
+ };
+ // `docked`'s background extends into the bottom/left/right insets while
+ // its content stays clear of them, via margin outside `Surface`'s own
+ // fixed-size box (so `Surface` grows to wrap it, keeping the icon row's
+ // 64dp band untouched). `floating` doesn't self-anchor, so it has no
+ // insets to account for.
+ const dockedInsetMargin = isDocked
+ ? {
+ marginBottom: insets.bottom,
+ marginLeft: insets.left,
+ marginRight: insets.right,
+ }
+ : null;
+
+ const pill = (
+
+
+ {withToolbarChildColors({ children, theme, colorScheme })}
+
+
+ );
+
+ // `floating` is positioned directly via `style` (like `FAB`'s `Shell`),
+ // no wrapper needed. `docked` anchors to its nearest positioned
+ // ancestor, which needs the wrapping `View` below.
+ if (!isDocked) {
+ return pill;
+ }
+
+ return (
+
+ {pill}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ content: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ column: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ dockedFill: {
+ width: '100%',
+ },
+ // `docked` anchors absolutely rather than reserving layout space, so
+ // consumers pad their own content to avoid it, same as `floating`.
+ dockedContainer: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ },
+});
+
+export default Toolbar;
diff --git a/src/components/Toolbar/tokens.ts b/src/components/Toolbar/tokens.ts
new file mode 100644
index 0000000000..88f75f5204
--- /dev/null
+++ b/src/components/Toolbar/tokens.ts
@@ -0,0 +1,86 @@
+import type { ColorRole, Elevation } from '../../theme/types';
+import type { ShapeToken } from '../../theme/utils/shape';
+
+/**
+ * MD3 Toolbar spec dimensions, shape, and color-role tokens.
+ * @see https://m3.material.io/components/toolbars/specs
+ */
+export type Variant = 'floating' | 'docked';
+
+export type ColorScheme = 'standard' | 'vibrant';
+
+export type Orientation = 'horizontal' | 'vertical';
+
+const docked = {
+ containerHeight: 64,
+ containerShape: 'none' as ShapeToken,
+ containerLeadingSpace: 16,
+ containerTrailingSpace: 16,
+ defaultSpacing: 32,
+} as const;
+
+const floating = {
+ containerHeight: 64,
+ containerShape: 'full' as ShapeToken,
+ containerLeadingSpace: 8,
+ containerTrailingSpace: 8,
+ defaultSpacing: 4,
+} as const;
+
+const elevation = {
+ docked: 0,
+ floating: 3,
+} as const satisfies Record;
+
+// A role that resolves differently per theme mode. Most roles below
+// happen to use the same one in `light`/`dark` (they're already
+// theme-aware on their own), but a couple of MD3's "Fixed" roles
+// (`on*FixedVariant`) are the wrong pick for `dark`: those are
+// *intentionally* the same tone in both modes, whereas the spec's actual
+// color for that slot isn't, so `light`/`dark` need to name different
+// roles there.
+type ToneRole = { light: ColorRole; dark: ColorRole };
+
+// Per https://m3.material.io/components/toolbars/specs—color roles for
+// the toolbar itself (`container`) and its mode-less children: `IconButton`
+// (`icon`, `buttonContainer`) and `Button` (`label`); a `mode` on either
+// (filled, outlined, etc.) opts it out in favor of its own mode-based
+// coloring instead (see `withToolbarChildColors`). `selected*` roles apply
+// only to `IconButton`—`Button` has no `selected` state, and an
+// unselected `IconButton` gets no `buttonContainer` override at all, since
+// it's the same role as the toolbar's own `container` (i.e. no visible
+// pill, it just blends in).
+const standardColors = {
+ container: { light: 'surfaceContainer', dark: 'surfaceContainer' },
+ buttonContainer: { light: 'surfaceContainer', dark: 'surfaceContainer' },
+ selectedButtonContainer: {
+ light: 'secondaryContainer',
+ dark: 'secondaryContainer',
+ },
+ icon: { light: 'onSurfaceVariant', dark: 'onSurfaceVariant' },
+ selectedIcon: {
+ light: 'onSecondaryFixedVariant',
+ dark: 'onSecondaryContainer',
+ },
+ label: { light: 'onSurfaceVariant', dark: 'onSurfaceVariant' },
+} as const satisfies Record;
+
+const vibrantColors = {
+ container: { light: 'primaryContainer', dark: 'primaryContainer' },
+ buttonContainer: { light: 'primaryContainer', dark: 'primaryContainer' },
+ selectedButtonContainer: {
+ light: 'surfaceContainer',
+ dark: 'surfaceContainer',
+ },
+ icon: { light: 'onPrimaryFixedVariant', dark: 'onPrimaryContainer' },
+ selectedIcon: { light: 'onSurface', dark: 'onSurface' },
+ label: { light: 'onPrimaryFixedVariant', dark: 'onPrimaryContainer' },
+} as const satisfies Record;
+
+export const ToolbarTokens = {
+ floating,
+ docked,
+ elevation,
+ standardColors,
+ vibrantColors,
+};
diff --git a/src/components/Toolbar/utils.ts b/src/components/Toolbar/utils.ts
new file mode 100644
index 0000000000..3a13433a64
--- /dev/null
+++ b/src/components/Toolbar/utils.ts
@@ -0,0 +1,181 @@
+import * as React from 'react';
+import type { ColorValue } from 'react-native';
+
+import { ToolbarTokens } from './tokens';
+import type { ColorScheme, Variant } from './tokens';
+import type { ColorRole, Elevation } from '../../theme/types';
+import type { InternalTheme } from '../../types';
+import Button from '../Button/Button';
+import IconButton from '../IconButton/IconButton';
+
+const resolveColors = (colorScheme: ColorScheme) =>
+ colorScheme === 'vibrant'
+ ? ToolbarTokens.vibrantColors
+ : ToolbarTokens.standardColors;
+
+/** Picks the role that applies for `theme`'s own light/dark mode. */
+const resolveTone = (
+ theme: InternalTheme,
+ role: { light: ColorRole; dark: ColorRole }
+): ColorValue => theme.colors[theme.dark ? role.dark : role.light];
+
+/** Resolve the container (background) color; an explicit `containerColor` wins over `colorScheme`. */
+export const resolveContainerColor = ({
+ theme,
+ colorScheme,
+ containerColor,
+}: {
+ theme: InternalTheme;
+ colorScheme: ColorScheme;
+ containerColor?: ColorValue;
+}): ColorValue => {
+ if (containerColor != null) {
+ return containerColor;
+ }
+
+ return resolveTone(theme, resolveColors(colorScheme).container);
+};
+
+/**
+ * Resolve an `IconButton`'s icon color and, if `selected`, its container
+ * color. Unselected has no container override, it's the same role as the
+ * toolbar's own background, so it just blends in.
+ */
+export const resolveIconColors = ({
+ theme,
+ colorScheme,
+ selected,
+}: {
+ theme: InternalTheme;
+ colorScheme: ColorScheme;
+ selected: boolean;
+}): { iconColor: ColorValue; containerColor?: ColorValue } => {
+ const roles = resolveColors(colorScheme);
+
+ return selected
+ ? {
+ iconColor: resolveTone(theme, roles.selectedIcon),
+ containerColor: resolveTone(theme, roles.selectedButtonContainer),
+ }
+ : { iconColor: resolveTone(theme, roles.icon) };
+};
+
+/** Resolve a `Button` child's label color (`Button` has no `selected` state, so there's just one). */
+export const resolveLabelColor = ({
+ theme,
+ colorScheme,
+}: {
+ theme: InternalTheme;
+ colorScheme: ColorScheme;
+}): ColorValue => resolveTone(theme, resolveColors(colorScheme).label);
+
+type RecolorableProps = {
+ children?: React.ReactNode;
+ mode?: unknown;
+ selected?: boolean;
+ iconColor?: ColorValue;
+ containerColor?: ColorValue;
+ textColor?: ColorValue;
+ buttonColor?: ColorValue;
+};
+
+const recolorChildren = (
+ children: React.ReactNode,
+ theme: InternalTheme,
+ colorScheme: ColorScheme
+): React.ReactNode =>
+ React.Children.map(children, (child) => {
+ if (!React.isValidElement(child)) {
+ return child;
+ }
+
+ // `React.Children.map` doesn't flatten a `Fragment`, so recurse into
+ // it manually.
+ if (child.type === React.Fragment) {
+ return React.cloneElement(
+ child,
+ undefined,
+ recolorChildren(child.props.children, theme, colorScheme)
+ );
+ }
+
+ if (child.type === IconButton) {
+ // A `mode` or explicit color prop means it already has its own
+ // spec-defined coloring.
+ if (
+ child.props.mode != null ||
+ child.props.iconColor != null ||
+ child.props.containerColor != null
+ ) {
+ return child;
+ }
+
+ const { iconColor, containerColor } = resolveIconColors({
+ theme,
+ colorScheme,
+ selected: child.props.selected ?? false,
+ });
+ return React.cloneElement(child, { iconColor, containerColor });
+ }
+
+ if (child.type === Button) {
+ // `text` is `Button`'s mode-less default; any other mode (its own
+ // spec-defined coloring) or an explicit color prop opts it out.
+ if (
+ (child.props.mode != null && child.props.mode !== 'text') ||
+ child.props.textColor != null ||
+ child.props.buttonColor != null
+ ) {
+ return child;
+ }
+
+ const textColor = resolveLabelColor({ theme, colorScheme });
+ return React.cloneElement(child, { textColor });
+ }
+
+ return child;
+ });
+
+/**
+ * Recolors every direct (including Fragment-nested), mode-less
+ * `IconButton`/`Button` child per `colorScheme`.
+ */
+export const withToolbarChildColors = ({
+ children,
+ theme,
+ colorScheme,
+}: {
+ children: React.ReactNode;
+ theme: InternalTheme;
+ colorScheme: ColorScheme;
+}): React.ReactNode => recolorChildren(children, theme, colorScheme);
+
+export const resolveElevation = ({
+ isDocked,
+}: {
+ isDocked: boolean;
+}): Elevation =>
+ isDocked ? ToolbarTokens.elevation.docked : ToolbarTokens.elevation.floating;
+
+/**
+ * Leading/trailing padding + inter-item gap for the content row/column,
+ * from spec defaults (`contentContainerStyle` can override). `docked`'s
+ * content row is a fixed 64dp band (see `Toolbar.tsx`'s `thickness`), so
+ * it only pads horizontally to leave room for taller children like a
+ * `Button` label; `floating` has no such fixed height, so it pads every
+ * side.
+ */
+export const getSpacing = ({
+ variant,
+}: {
+ variant: Variant;
+}): { paddingLeading: number; paddingTrailing: number; gap: number } => {
+ const tokens =
+ variant === 'docked' ? ToolbarTokens.docked : ToolbarTokens.floating;
+
+ return {
+ paddingLeading: tokens.containerLeadingSpace,
+ paddingTrailing: tokens.containerTrailingSpace,
+ gap: tokens.defaultSpacing,
+ };
+};
diff --git a/src/components/__tests__/Toolbar.test.tsx b/src/components/__tests__/Toolbar.test.tsx
new file mode 100644
index 0000000000..28b9434810
--- /dev/null
+++ b/src/components/__tests__/Toolbar.test.tsx
@@ -0,0 +1,384 @@
+import { Platform } from 'react-native';
+
+import { describe, expect, it } from '@jest/globals';
+import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
+
+import { getTheme } from '../../core/theming';
+import { render, screen } from '../../test-utils';
+import Button from '../Button/Button';
+import IconButton from '../IconButton/IconButton';
+import type { ColorScheme } from '../Toolbar/tokens';
+import Toolbar from '../Toolbar/Toolbar';
+import {
+ resolveContainerColor,
+ resolveIconColors,
+ resolveLabelColor,
+} from '../Toolbar/utils';
+
+type Field =
+ | 'container'
+ | 'label'
+ | 'icon'
+ | 'selectedIcon'
+ | 'selectedContainer';
+
+const ToolbarChildren = () => (
+ <>
+ {}} />
+ {}} />
+ >
+);
+
+it('renders Toolbar with default props', async () => {
+ const tree = (
+ await render(
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders Toolbar with docked variant', async () => {
+ const tree = (
+ await render(
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('extends the docked container into the bottom safe-area inset', async () => {
+ const tree = (
+ await render(
+
+
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders Toolbar with vertical orientation', async () => {
+ const tree = (
+ await render(
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it("applies `style` directly to the floating pill's outer (position) layer", async () => {
+ const originalPlatform = Platform.OS;
+ Platform.OS = 'ios';
+
+ await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('floating-outer-layer')).toHaveStyle({
+ position: 'absolute',
+ top: 5,
+ });
+
+ Platform.OS = originalPlatform;
+});
+
+it("lets `style` override the floating pill's internal container styles", async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('floating-outer-layer')).toHaveStyle({
+ height: 200,
+ });
+});
+
+it("applies `style` to the docked variant's self-anchoring container", async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('docked-container')).toHaveStyle({ bottom: 10 });
+});
+
+it('renders floating Toolbar with vibrant colorScheme', async () => {
+ const tree = (
+ await render(
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders docked Toolbar with vibrant colorScheme', async () => {
+ await render(
+ <>
+
+
+
+
+
+
+ >
+ );
+
+ const theme = getTheme();
+ expect(screen.getByTestId('toolbar-vibrant')).toHaveStyle({
+ backgroundColor: theme.colors.primaryContainer,
+ });
+ expect(screen.getByTestId('toolbar-standard')).toHaveStyle({
+ backgroundColor: theme.colors.surfaceContainer,
+ });
+});
+
+it('keeps an explicit iconColor instead of the vibrant default', async () => {
+ const tree = (
+ await render(
+
+ {}} />
+ {}} />
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it("defaults a mode-less IconButton child's iconColor to the toolbar's content color", async () => {
+ const tree = (
+ await render(
+
+ {}} />
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('leaves an IconButton child with an explicit mode uncolored', async () => {
+ const tree = (
+ await render(
+
+ {}} />
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('recolors IconButtons through a Fragment held in a variable, not just direct children', async () => {
+ const fragmentChildren = (
+ <>
+ {}} />
+ {}} />
+ >
+ );
+ const tree = (
+ await render({fragmentChildren})
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it('gives a selected IconButton child the selected container color', async () => {
+ await render(
+ <>
+
+ {}}
+ />
+
+
+ {}}
+ />
+
+ >
+ );
+
+ const theme = getTheme();
+ expect(screen.getByTestId('standard-selected-container')).toHaveStyle({
+ backgroundColor: theme.colors.secondaryContainer,
+ });
+ expect(screen.getByTestId('vibrant-selected-container')).toHaveStyle({
+ backgroundColor: theme.colors.surfaceContainer,
+ });
+});
+
+it("leaves a selected IconButton child's explicit containerColor untouched", async () => {
+ await render(
+
+ {}}
+ />
+
+ );
+
+ expect(screen.getByTestId('explicit-container')).toHaveStyle({
+ backgroundColor: 'red',
+ });
+});
+
+it("defaults a mode-less Button child's textColor to the toolbar's content color", async () => {
+ await render(
+ <>
+
+
+
+
+
+
+ >
+ );
+
+ const theme = getTheme();
+ expect(screen.getByTestId('standard-text')).toHaveStyle({
+ color: theme.colors.onSurfaceVariant,
+ });
+ expect(screen.getByTestId('vibrant-text')).toHaveStyle({
+ color: theme.colors.onPrimaryFixedVariant,
+ });
+});
+
+it('recolors a Button child with an explicit mode="text", same as no mode', async () => {
+ await render(
+
+
+
+ );
+
+ const theme = getTheme();
+ expect(screen.getByTestId('text-mode-text')).toHaveStyle({
+ color: theme.colors.onPrimaryFixedVariant,
+ });
+});
+
+it('leaves a Button child with a more opinionated mode uncolored', async () => {
+ const tree = (
+ await render(
+
+
+
+ )
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+});
+
+it("leaves a Button child's explicit textColor/buttonColor untouched", async () => {
+ await render(
+
+
+
+
+ );
+
+ expect(screen.getByTestId('explicit-text-text')).toHaveStyle({
+ color: 'red',
+ });
+ expect(screen.getByTestId('explicit-button-color-container')).toHaveStyle({
+ backgroundColor: 'blue',
+ });
+});
+
+// `theme.colors` values are `rgba(r, g, b, 1)` strings convert to hex to
+// compare against the design spec's hex values directly.
+const toHex = (rgba: unknown) => {
+ const [r, g, b] = String(rgba).match(/\d+/g)!.map(Number);
+ return (
+ '#' +
+ [r, g, b]
+ .map((n) => n.toString(16).padStart(2, '0'))
+ .join('')
+ .toUpperCase()
+ );
+};
+
+describe('color resolution across light/dark themes', () => {
+ const light = getTheme(false);
+ const dark = getTheme(true);
+
+ it.each([
+ ['standard', 'light', 'container', '#F3EDF7'],
+ ['standard', 'dark', 'container', '#211F26'],
+ ['standard', 'light', 'icon', '#49454F'],
+ ['standard', 'dark', 'icon', '#CAC4D0'],
+ ['standard', 'light', 'label', '#49454F'],
+ ['standard', 'dark', 'label', '#CAC4D0'],
+ ['standard', 'light', 'selectedContainer', '#E8DEF8'],
+ ['standard', 'dark', 'selectedContainer', '#4A4458'],
+ ['standard', 'light', 'selectedIcon', '#4A4458'],
+ ['standard', 'dark', 'selectedIcon', '#E8DEF8'],
+ ['vibrant', 'light', 'container', '#EADDFF'],
+ ['vibrant', 'dark', 'container', '#4F378B'],
+ ['vibrant', 'light', 'icon', '#4F378B'],
+ ['vibrant', 'dark', 'icon', '#EADDFF'],
+ ['vibrant', 'light', 'label', '#4F378B'],
+ ['vibrant', 'dark', 'label', '#EADDFF'],
+ ['vibrant', 'light', 'selectedContainer', '#F3EDF7'],
+ ['vibrant', 'dark', 'selectedContainer', '#211F26'],
+ ['vibrant', 'light', 'selectedIcon', '#1D1B20'],
+ ['vibrant', 'dark', 'selectedIcon', '#E6E0E9'],
+ ] as Array<[ColorScheme, 'light' | 'dark', Field, string]>)(
+ 'resolves %s %s %s to %s',
+ (colorScheme, mode, field, expectedHex) => {
+ const theme = mode === 'dark' ? dark : light;
+ const resolved =
+ field === 'container'
+ ? resolveContainerColor({ theme, colorScheme })
+ : field === 'label'
+ ? resolveLabelColor({ theme, colorScheme })
+ : field === 'selectedContainer'
+ ? resolveIconColors({ theme, colorScheme, selected: true })
+ .containerColor
+ : field === 'selectedIcon'
+ ? resolveIconColors({ theme, colorScheme, selected: true })
+ .iconColor
+ : resolveIconColors({ theme, colorScheme, selected: false })
+ .iconColor;
+
+ expect(toHex(resolved)).toBe(expectedHex);
+ }
+ );
+});
diff --git a/src/components/__tests__/__snapshots__/Toolbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Toolbar.test.tsx.snap
new file mode 100644
index 0000000000..a78639150f
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/Toolbar.test.tsx.snap
@@ -0,0 +1,3115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`defaults a mode-less IconButton child's iconColor to the toolbar's content color 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+`;
+
+exports[`extends the docked container into the bottom safe-area inset 1`] = `
+
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`keeps an explicit iconColor instead of the vibrant default 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+`;
+
+exports[`leaves a Button child with a more opinionated mode uncolored 1`] = `
+
+
+
+
+
+
+
+
+ Done
+
+
+
+
+
+
+
+
+`;
+
+exports[`leaves an IconButton child with an explicit mode uncolored 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+`;
+
+exports[`recolors IconButtons through a Fragment held in a variable, not just direct children 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders Toolbar with default props 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders Toolbar with docked variant 1`] = `
+
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders Toolbar with vertical orientation 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders floating Toolbar with vibrant colorScheme 1`] = `
+
+
+
+
+
+
+
+
+ format-bold
+
+
+
+
+
+
+
+
+
+
+ format-italic
+
+
+
+
+
+
+
+
+`;
diff --git a/src/index.tsx b/src/index.tsx
index 8863e2fa20..017bf9564f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -46,6 +46,7 @@ export { default as Surface } from './components/Surface';
export { default as Switch } from './components/Switch/Switch';
export { default as Appbar } from './components/Appbar';
export { default as TouchableRipple } from './components/TouchableRipple/TouchableRipple';
+export { default as Toolbar } from './components/Toolbar/Toolbar';
export { default as TextInput } from './components/TextInput';
export { default as ToggleButton } from './components/ToggleButton';
export { default as SegmentedButtons } from './components/SegmentedButtons/SegmentedButtons';
@@ -145,6 +146,12 @@ export type { Props as TouchableRippleProps } from './components/TouchableRipple
export type { Props as TextProps } from './components/Typography/Text';
export type { Props as SegmentedButtonsProps } from './components/SegmentedButtons/SegmentedButtons';
export type { Props as ListImageProps } from './components/List/ListImage';
+export type { Props as ToolbarProps } from './components/Toolbar/Toolbar';
+export type {
+ Variant as ToolbarVariant,
+ ColorScheme as ToolbarColorScheme,
+ Orientation as ToolbarOrientation,
+} from './components/Toolbar/tokens';
export type { Props as TooltipProps } from './components/Tooltip/Tooltip';
export { type TypescaleKey, type Theme, type Elevation } from './types';