Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ and read the whole release rather than scanning for a label.
Prior history: these packages were published as `@onlynative/*` through
`0.0.0-alpha.8`. The `@rootnative` line below starts over at `0.0.0-alpha.0`.

## Unreleased

**No prop is removed.** One new variant in `@rootnative/components`. One
type change can stop a build: an `interface` that extends
`NavigationBarProps`.

- **New: `NavigationBar` `variant="floating"`.** A capsule that floats over
the content: 64dp high, at most 480dp wide, with a 16dp gap to each side
and to the bottom of the window. Each destination shows its icon only,
and its label is the screen-reader name. The active indicator fills the
destination. The bar places itself at the bottom of its parent and reads
the safe-area insets from a `SafeAreaProvider`. It floats over a bottom
inset of 40dp or less, which holds only a gesture handle, and it stays
16dp above a taller inset. MD3 has no floating navigation bar. The
capsule takes the MD3 colors, and the height, the edge gap and the item
gap of the MD3 floating toolbar. Its elevation, level 3, is not an MD3
value for this surface. The default `variant` is `'docked'`, the bar
as before. `labelVisibility`, `labelStyle` and `insetBottom` apply to the
docked bar only, and the type rejects them on the floating bar. The new
type `NavigationBarVariant` names the two values.

- **`NavigationBarProps` is now a type alias**, so that the type can reject
the docked props on the floating bar. An `interface` cannot extend a
union, so an `interface` that extends `NavigationBarProps` does not
compile. Use an intersection in its place:
`type MyBarProps = NavigationBarProps & { … }`.

## 0.0.0-alpha.34 — 2026-11-03

**No breaking change.** One new component, `Carousel`, in
Expand Down
1 change: 1 addition & 0 deletions api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"NavigationBarItem",
"NavigationBarLabelVisibility",
"NavigationBarProps",
"NavigationBarVariant",
"NavigationDrawer",
"NavigationDrawerDivider",
"NavigationDrawerDividerProps",
Expand Down
65 changes: 61 additions & 4 deletions docs/docs/components/navigation-bar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Follows the [Material Design 3 Navigation bar](https://m3.material.io/components
specification.

Each destination is an icon over a label, with the active one marked by a `secondaryContainer`
pill behind its icon. The bar is 80dp tall on a `surfaceContainer` background.
pill behind its icon. The bar is 80dp tall on a `surfaceContainer` background. The
[floating](#floating) variant is a 64dp capsule over the content, with an icon for each
destination.

:::info NavigationBar is a bar, not a navigator
`NavigationBar` renders the destinations and tells you which one was pressed. It does not own
Expand Down Expand Up @@ -140,7 +142,49 @@ inside the same pill shape, on active and inactive destinations alike.

A bottom-anchored bar must clear the home indicator. Pass `insetBottom` to wrap the row in a
`SafeAreaView` that adds the bottom inset below the bar's 80dp — leave it off (the default) when
an ancestor already applies the bottom edge, or the inset would be added twice.
an ancestor already applies the bottom edge, or the inset would be added twice. The floating bar
reads the insets by itself: see [Floating](#floating).

## Floating

`variant="floating"` draws the bar as a capsule that floats over the content. MD3 has no
floating navigation bar. This form takes the MD3 colors, and the height, the gap to the window
edges and the item gap of the MD3 floating toolbar: 64dp, 16dp and 4dp. Its level 3 elevation is
not an MD3 value for this surface.

- The capsule is 64dp high and at most 480dp wide, with a 16dp gap to each side and to the
bottom of the window.
- Each destination shows its icon only. The label is the screen-reader name of the destination.
- The active indicator fills the destination.

The bar places itself at the bottom of its parent, over the content. Make it the last child of a
view that reaches the bottom edge of the window. Pad the end of the content, so that the last
item can scroll clear of the bar: 80dp (the 64dp capsule and the 16dp gap), plus the bottom
inset when the inset is more than 40dp.

```tsx
<View style={styles.screen}>
<ScrollView contentContainerStyle={styles.content}>{/* … */}</ScrollView>
<NavigationBar
variant="floating"
items={DESTINATIONS}
value={destination}
onValueChange={setDestination}
accessibilityLabel="Main navigation"
/>
</View>
```

The bar reads the safe-area insets from a `SafeAreaProvider`. Expo Router adds one.

- A bottom inset of 40dp or less holds only a gesture handle: the iOS home indicator or the
Android gesture bar. The capsule floats over it, 16dp from the bottom edge.
- A taller bottom inset holds the Android navigation buttons. The capsule stays 16dp above it.
- The side insets are added to the 16dp gap.
- Without a provider, the capsule keeps the 16dp gap.

`labelVisibility`, `labelStyle` and `insetBottom` apply to the docked bar only. The type rejects
them with `variant="floating"`.

## Overrides

Expand All @@ -151,7 +195,7 @@ an ancestor already applies the bottom edge, or the inset would be added twice.
| `selectedContentColor` | Icon and label color of the active destination; the state layers re-derive from it |
| `indicatorColor` | The active indicator pill |
| `labelStyle` | Every destination's label `Text` — does not affect the icons |
| `style` | The bar container |
| `style` | The bar container; on the floating bar, the capsule |

Disabled destinations always use the MD3 disabled treatment (38% content); no override changes
that.
Expand All @@ -175,6 +219,18 @@ These are the Expressive-updated androidx tokens: the indicator is 56dp wide (no
pre-Expressive 64dp) and the active label is `secondary` (not `onSurface`). The 64dp-tall
Expressive short bar is a possible 1.x variant; this bar uses compose's `TallContainerHeight`.

The floating bar changes these:

| Token | Value |
|-------|-------|
| Container | `surfaceContainer`, elevation level 3, full corners |
| Height | 64dp |
| Width | The parent less a 16dp gap on each side, at most 480dp |
| Bottom gap | 16dp from the bottom edge, or 16dp above a bottom inset of more than 40dp |
| Padding | 6dp around the destinations, 4dp between them |
| Active indicator | Fills the destination, `secondaryContainer`, full corners |
| Label | None; the label is the screen-reader name |

## Accessibility

- The bar reports `role="tablist"` and each destination `role="tab"` with
Expand All @@ -184,7 +240,8 @@ Expressive short bar is a possible 1.x variant; this bar uses compose's `TallCon
hides the visible label; set `accessibilityLabel` on the item to override it.
- Disabled destinations expose `accessibilityState.disabled` and stop responding.
- On web, destinations show a keyboard focus ring (`secondary`, 3dp) around the indicator pill,
driven by the same focus-visible state as the rest of the library.
driven by the same focus-visible state as the rest of the library. On the floating bar, the
ring goes around the destination.

## Props

Expand Down
17 changes: 13 additions & 4 deletions docs/static/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2463,23 +2463,32 @@ import { NavigationBar } from '@rootnative/components/navigation-bar'
// (active icon+label), indicatorColor (pill), labelStyle. Disabled items stay
// at the MD3 38%.
<NavigationBar indicatorColor="#FFD8E4" items={items} />

// Floating: a 64dp capsule over the content, icons only, 16dp from the window
// edges. It places itself at the bottom of its parent and reads the safe-area
// insets: a bottom inset of 40 or less (a gesture handle) goes under it.
<View style={{ flex: 1 }}>
<ScrollView>{/* content: pad its end by 80 */}</ScrollView>
<NavigationBar variant="floating" items={items} value={destination} onValueChange={setDestination} />
</View>
```

Props:
- `items: NavigationBarItem[]` — The destinations to render. MD3 recommends 3–5.
- `value?: string` — Value of the active destination (controlled).
- `defaultValue?: string` — Value of the initially active destination (uncontrolled). Defaults to the first item — a navigation bar with nothing active reads as broken.
- `onValueChange?: (value: string) => void` — Called with the value of the destination that was pressed.
- `labelVisibility?: 'always' | 'selected' | 'never'` — Default: `'always'`. When destination labels are shown.
- `insetBottom?: boolean` — Default: `false`. When `true`, wraps the row in a SafeAreaView that adds the bottom inset below the bar's 80dp, so it clears the home indicator. Leave it off when an ancestor (e.g. `Layout` with a `bottom` edge) already handles it.
- `containerColor?: string` — Default: `surfaceContainer`. Override the bar background.
- `contentColor?: string` — Default: `onSurfaceVariant`. Override the icon and label color of inactive destinations.
- `selectedContentColor?: string` — Default: `onSecondaryContainer (icon) / secondary (label)`. Override the icon and label color of the active destination. State-layer colors are derived from it automatically.
- `indicatorColor?: string` — Default: `secondaryContainer`. Override the active indicator pill color.
- `labelStyle?: StyleProp<TextStyle>` — Style applied to every destination's label `Text` — does not affect the icons.
- `style?: StyleProp<ViewStyle>` — Style applied to the bar container.
- `style?: StyleProp<ViewStyle>` — Style applied to the bar container. On the floating bar, that is the capsule.
- `accessibilityLabel?: string` — Screen-reader label for the bar itself.
- `testID?: string` — Test id applied to the bar container. Each destination carries `<testID>-item-<value>` and its indicator `<testID>-item-<value>-indicator`.
- `variant?: 'docked' | 'floating'` — Default: `'docked'`. `'docked'` is the MD3 navigation bar, an 80dp band along the bottom edge. `'floating'` is a 64dp capsule over the content, at most 480 wide, 16dp from the window edges, with an icon for each destination and no visible label; the indicator fills the destination. It places itself at the bottom of its parent and reads the insets from a `SafeAreaProvider`: it floats over a bottom inset of 40 or less and stays 16dp above a taller one.
- `labelVisibility?: 'always' | 'selected' | 'never'` — Default: `'always'`. When destination labels are shown. Docked only.
- `insetBottom?: boolean` — Default: `false`. Adds the bottom safe-area inset below the bar's 80dp. Leave it off when an ancestor already handles the edge. Docked only.
- `labelStyle?: StyleProp<TextStyle>` — Style for every label `Text`. Docked only.
- Inherits `ViewProps` (except `children`)

#### NavigationBarItem
Expand Down
58 changes: 58 additions & 0 deletions example/app/navigation-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useTheme } from '@rootnative/core'
import { useMemo, useState } from 'react'
import { ScrollView, StyleSheet } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { catalogMetadata } from '../src/metadata'
import { ScreenIntro } from '../src/ScreenIntro'
import { ScreenNavFooter } from '../src/ScreenNavFooter'
Expand Down Expand Up @@ -36,6 +37,35 @@ const PANELS: Record<string, string> = {
profile: 'Your account and settings.',
}

// A nested `SafeAreaProvider` measures the insets of its own frame, so the
// capsule keeps its gap to the frame and not to the window.
function FloatingDemo() {
const [destination, setDestination] = useState('home')

return (
<SafeAreaProvider style={styles.floatingFrame}>
<ScrollView
nestedScrollEnabled
contentContainerStyle={styles.floatingScrollContent}
>
<Typography variant="bodyMedium">{PANELS[destination]}</Typography>
{Array.from({ length: 12 }, (_, index) => (
<Typography key={index} variant="bodyMedium">
Scroll content row {index + 1}
</Typography>
))}
</ScrollView>
<NavigationBar
variant="floating"
items={DESTINATIONS}
value={destination}
onValueChange={setDestination}
accessibilityLabel="Floating navigation"
/>
</SafeAreaProvider>
)
}

function NavigationBarScreenContent() {
const theme = useTheme()
const [destination, setDestination] = useState('home')
Expand Down Expand Up @@ -108,6 +138,22 @@ function NavigationBarScreenContent() {
</Card>
</Column>

<Column gap="sm">
<Typography variant="titleMedium">Floating</Typography>
<Card variant="outlined">
<Column gap="sm">
<Column p="md">
<Typography variant="bodySmall" style={mutedText}>
variant=&quot;floating&quot; is a capsule over the content,
16dp from the edges, with an icon for each destination. The
label is the screen-reader name.
</Typography>
</Column>
<FloatingDemo />
</Column>
</Card>
</Column>

<Column gap="sm">
<Typography variant="titleMedium">Disabled and overrides</Typography>
<Card variant="outlined">
Expand Down Expand Up @@ -151,4 +197,16 @@ const styles = StyleSheet.create({
scroll: {
flexGrow: 1,
},
floatingFrame: {
flex: 0,
height: 320,
overflow: 'hidden',
},
// 16 of padding plus the 64dp capsule and its 16dp gap, so the last row
// scrolls clear of it.
floatingScrollContent: {
padding: 16,
paddingBottom: 112,
rowGap: 16,
},
})
17 changes: 13 additions & 4 deletions packages/components/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1803,23 +1803,32 @@ import { NavigationBar } from '@rootnative/components/navigation-bar'
// (active icon+label), indicatorColor (pill), labelStyle. Disabled items stay
// at the MD3 38%.
<NavigationBar indicatorColor="#FFD8E4" items={items} />

// Floating: a 64dp capsule over the content, icons only, 16dp from the window
// edges. It places itself at the bottom of its parent and reads the safe-area
// insets: a bottom inset of 40 or less (a gesture handle) goes under it.
<View style={{ flex: 1 }}>
<ScrollView>{/* content: pad its end by 80 */}</ScrollView>
<NavigationBar variant="floating" items={items} value={destination} onValueChange={setDestination} />
</View>
```

Props:
- `items: NavigationBarItem[]` — The destinations to render. MD3 recommends 3–5.
- `value?: string` — Value of the active destination (controlled).
- `defaultValue?: string` — Value of the initially active destination (uncontrolled). Defaults to the first item — a navigation bar with nothing active reads as broken.
- `onValueChange?: (value: string) => void` — Called with the value of the destination that was pressed.
- `labelVisibility?: 'always' | 'selected' | 'never'` — Default: `'always'`. When destination labels are shown.
- `insetBottom?: boolean` — Default: `false`. When `true`, wraps the row in a SafeAreaView that adds the bottom inset below the bar's 80dp, so it clears the home indicator. Leave it off when an ancestor (e.g. `Layout` with a `bottom` edge) already handles it.
- `containerColor?: string` — Default: `surfaceContainer`. Override the bar background.
- `contentColor?: string` — Default: `onSurfaceVariant`. Override the icon and label color of inactive destinations.
- `selectedContentColor?: string` — Default: `onSecondaryContainer (icon) / secondary (label)`. Override the icon and label color of the active destination. State-layer colors are derived from it automatically.
- `indicatorColor?: string` — Default: `secondaryContainer`. Override the active indicator pill color.
- `labelStyle?: StyleProp<TextStyle>` — Style applied to every destination's label `Text` — does not affect the icons.
- `style?: StyleProp<ViewStyle>` — Style applied to the bar container.
- `style?: StyleProp<ViewStyle>` — Style applied to the bar container. On the floating bar, that is the capsule.
- `accessibilityLabel?: string` — Screen-reader label for the bar itself.
- `testID?: string` — Test id applied to the bar container. Each destination carries `<testID>-item-<value>` and its indicator `<testID>-item-<value>-indicator`.
- `variant?: 'docked' | 'floating'` — Default: `'docked'`. `'docked'` is the MD3 navigation bar, an 80dp band along the bottom edge. `'floating'` is a 64dp capsule over the content, at most 480 wide, 16dp from the window edges, with an icon for each destination and no visible label; the indicator fills the destination. It places itself at the bottom of its parent and reads the insets from a `SafeAreaProvider`: it floats over a bottom inset of 40 or less and stays 16dp above a taller one.
- `labelVisibility?: 'always' | 'selected' | 'never'` — Default: `'always'`. When destination labels are shown. Docked only.
- `insetBottom?: boolean` — Default: `false`. Adds the bottom safe-area inset below the bar's 80dp. Leave it off when an ancestor already handles the edge. Docked only.
- `labelStyle?: StyleProp<TextStyle>` — Style for every label `Text`. Docked only.
- Inherits `ViewProps` (except `children`)

#### NavigationBarItem
Expand Down
Loading
Loading