Skip to content

feat: implement Toolbar component - #5043

Open
k0ndee wants to merge 15 commits into
callstack:mainfrom
k0ndee:@k0ndee/toolbar
Open

k0ndee wants to merge 15 commits into
callstack:mainfrom
k0ndee:@k0ndee/toolbar

Conversation

@k0ndee

@k0ndee k0ndee commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

Introduces Toolbar, implementing the Material Design 3 toolbars spec. It reuses existing theme tokens (shape, color roles, elevation) and follows the same per-component-tokens pattern as FAB/Checkbox.

Spec re-check (M3 toolbars)

Re-checked the M3 toolbars spec:

  • Two variants: floating (self-positioned pill, corner.full, elevation level 3) and docked (full-width bar pinned to the bottom edge, corner.none, no elevation, extends into safe-area insets).
  • floating supports horizontal/vertical orientation; docked is always horizontal per spec.
  • standard (default) colors: container/unselected-button surfaceContainer, icon/label onSurfaceVariant, selected button secondaryContainer with onSecondaryContainer icon.
  • vibrant colorScheme: container/unselected-button primaryContainer, icon/label onPrimaryContainer, selected button falls back to surfaceContainer with onSurface icon.
  • Mode-less IconButton/Button descendants pick up the toolbar's colorScheme automatically via context - not just direct children - unless they already set their own mode or an explicit color prop.

Changes

Toolbar / tokens / utils

  • New src/components/Toolbar/{Toolbar.tsx,tokens.ts,utils.ts,ToolbarColorContext.ts}: variant (floating/docked), orientation (horizontal/vertical, floating-only), colorScheme (standard/vibrant), containerColor override, style/contentContainerStyle, testID, aria-label, theme, ref
  • Container shape/elevation/spacing resolved from ToolbarTokens. docked's outer wrapper has no inset margin, so its background naturally spans edge-to-edge into the safe area, while the content row gets insets-based margin to stay clear of it - the 64dp icon band itself stays untouched
  • ToolbarColorContext carries theme/colorScheme down to descendants; IconButton/Button each read it directly and resolve their own color, backing off if they already have a mode or explicit color prop
  • Exported from src/index.tsx

Example / docs / tests

  • Example screen (ToolbarExample.tsx) covering both variants, both orientations, both color schemes, over a scrollable list
  • Prop docs and a theme-colors table entry (auto-generated docs page); screenshots for floating (horizontal/vertical) and docked, each in standard/vibrant
  • Unit tests (Toolbar.test.tsx) covering shape/elevation per variant, color resolution across light/dark themes, and child auto-recoloring (including nested/non-direct descendants, opt-out via mode or explicit color)

Scope note

Per Satyajit Sahoo's recommendation, this lands as a series of PRs instead of one large change. This is PR 1 of the series:

  • PR 1 (this PR) - Floating + docked variant, static (no animation, no FAB pairing), colorScheme standard/vibrant. Component, tokens, tests, docs, example.
  • PR 2 - Show/hide animation (offscreen slide + spring), generic scroll shared-value primitive.
  • PR 3 - FAB pairing, FAB/key-action collapse on scroll.
  • PR 4 - Trailing-edge overflow menu.

Related issue

Related to #4988

Test plan

  • yarn typecheck / yarn lint / Toolbar unit tests
  • Full unit test suite green
  • Manually verified in the example app: floating (horizontal/vertical) and docked, standard/vibrant, over scrolling content, light and dark themes

Visual verification

standard vibrant
floating, horizontal floating-h-standard floating-h-vibrant
floating, vertical floating-v-standard floating-v-vibrant
docked docked-standard docked-vibrant

@k0ndee k0ndee changed the title @k0ndee/toolbar feat: implement Toolbar component Aug 11, 2026

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work! 👍 overall, this looks like a solid foundation for the new Toolbar components
just a few points to address before merge:

Comment thread src/components/Toolbar/tokens.ts Outdated
Comment thread src/components/Toolbar/utils.ts Outdated
Comment thread src/components/Toolbar/Toolbar.tsx Outdated

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Comment thread src/components/Toolbar/utils.ts Outdated
Comment thread src/components/Toolbar/Toolbar.tsx Outdated
Comment thread docs/src/data/themeColors.ts Outdated
Comment thread src/components/Toolbar/tokens.ts Outdated

@JKobrynski JKobrynski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor comment

Comment thread src/components/__tests__/Toolbar.test.tsx

@JKobrynski JKobrynski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the last one, and it's minor

Comment thread src/components/Toolbar/Toolbar.tsx
@JKobrynski

Copy link
Copy Markdown
Collaborator

LGTM! @satya164 do you want to take a look?

@@ -0,0 +1,3388 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to rerun these with -u after rebasing, because default testIDs were removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, the PR was still behind main branch. I rebased it and now it is up to date.

Comment thread src/components/Toolbar/Toolbar.tsx Outdated
* Style for positioning `floating`'s pill, or overriding `docked`'s
* default anchoring.
*/
style?: StyleProp<ViewStyle>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when you pass backgroundColor|borderRadius through this prop? I'm afraid they'd silently do nothing, other components type style as styleProp<SurfaceStyle>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, fixed it by changing style type to StyleProp<SurfaceStyle>

Comment thread src/components/Toolbar/Toolbar.tsx Outdated
import { resolveCornerRadius } from '../../theme/utils/shape';
import Surface from '../Surface';

export type Props = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other components use Omit<ViewProps, 'style'> to allow props like onLayout`, does that work here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it, changed Props to Omit<ViewProps, 'style'>

Comment thread example/src/Examples/ToolbarExample.tsx Outdated
Comment on lines +163 to +164
? [styles.verticalAnchor, { right: insets.right + 16 }]
: [styles.horizontalAnchor, { bottom: insets.bottom + 24 }]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked the edge offsets against the toolbar specs? I'm afraid the two are swapped

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The horizontal edge offset is incorrect, it should be 16. As for the vertical one the only spec in MD3 is that the minimum offset from screen edge should be equal to 16 so I left it as it is.

ref={ref}
pointerEvents="box-none"
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
style={[styles.dockedContainer, style as StyleProp<ViewStyle>]}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to use as here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's needed. style is typed as StyleProp<SurfaceStyle>, and SurfaceStyle wraps ViewStyle in Reanimated's AnimatedStyle (so it typechecks when forwarded to Surface, which is an Animated.View). Here it's applied to a plain View instead, which only accepts plain ViewStyle - hence the mismatch.

The cast is safe in practice: this wrapper isn't animated, so no shared-value-typed style ever reaches it. I also checked whether swapping this to Reanimated's Animated.View would avoid the cast - it does typecheck, but it always renders with collapsable={false}, which is a permanent Android view-flattening cost for every Toolbar (docked or not) just to accommodate a case that doesn't apply here.

@satya164
satya164 requested a balanced review from Copilot September 15, 2026 20:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants