Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/sf-symbol-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"react-native-bottom-tabs": minor
"@bottom-tabs/react-navigation": minor
---

Support the full set of SF Symbol configuration options on tab icons.

`AppleIcon` now accepts `size`, `color`, `weight`, `scale`, `variableValue`, `variableValueMode`, `renderingMode`, `colors` and `colorRenderingMode` alongside `sfSymbol`, matching the options React Navigation exposes for SF Symbols. Symbol effects and content transitions are not included, because a tab bar item renders a still image and never runs symbol animations.

Existing icons are unaffected: with no options set, symbols render exactly as before.
2 changes: 1 addition & 1 deletion docs/docs/docs/guides/_meta.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["usage-with-react-navigation", "usage-with-expo-router", "usage-with-one", "standalone-usage", {"type": "divider"}, "web-platform-support", "handling-scrollview-insets", "usage-with-vector-icons", {"type": "divider"}, "android-native-styling", "edge-to-edge-support"]
["usage-with-react-navigation", "usage-with-expo-router", "usage-with-one", "standalone-usage", {"type": "divider"}, "web-platform-support", "handling-scrollview-insets", "usage-with-vector-icons", "sf-symbols", {"type": "divider"}, "android-native-styling", "edge-to-edge-support"]
173 changes: 173 additions & 0 deletions docs/docs/docs/guides/sf-symbols.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Badge } from '@theme';

# SF Symbols

Tab icons on Apple platforms can be [SF Symbols](https://developer.apple.com/sf-symbols/) instead of images. Pass an object with an `sfSymbol` key wherever an icon is accepted, and add any of the configuration options below alongside it.

```tsx
focusedIcon: { sfSymbol: 'house.fill' }
```

:::note
SF Symbols are only available on Apple platforms. On Android and web, pass an image with `require()` or a `{ uri }` object instead.
:::

The options mirror the ones [React Navigation exposes for SF Symbols](https://reactnavigation.org/docs/8.x/icons/#sf-symbols), so an icon configured for one works the same here.

## Options

Only `sfSymbol` is required. Leave the rest out to keep the system defaults, which is what you want for most tabs.

### `sfSymbol`

Name of the symbol to display, for example `house` or `house.fill`. Browse the full set in Apple's [SF Symbols app](https://developer.apple.com/sf-symbols/).

- Type: `SFSymbol`

### `size`

Point size of the symbol.

- Type: `number`
- Default: the size the tab bar picks for the current platform

### `color`

Color of the symbol. Used as the tint in `monochrome` mode, and as the fallback for `colors.primary` in `hierarchical` and `palette` modes.

Setting it opts the icon out of `tabBarActiveTintColor` and `tabBarInactiveTintColor`, since the symbol then carries a color of its own.

- Type: `ColorValue`

### `weight`

Stroke weight of the symbol. Accepts a name or its numeric equivalent.

- Type: `'thin' | 'ultralight' | 'light' | 'regular' | 'medium' | 'semibold' | 'bold' | 'extrabold' | 'black' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900`
- Default: `'regular'`

```tsx
focusedIcon: { sfSymbol: 'star.fill', weight: 'semibold' }
```

### `scale`

Scale variant of the symbol, relative to the surrounding text.

- Type: `'small' | 'medium' | 'large'`
- Default: `'medium'`

### `variableValue`

Value between `0` and `1` used to customize variable symbols.

Variable symbols such as `wifi` or `speaker.wave.3` have layers that activate progressively to represent a magnitude. `0` renders the fewest layers, `1` the full symbol. It has no effect on symbols that are not variable.

- Type: `number`
- Requires iOS 16 or later

```tsx
focusedIcon: { sfSymbol: 'wifi', variableValue: 0.6 }
```

### `variableValueMode` <Badge text="iOS 26+" type="info" />

How the partial state described by `variableValue` is rendered.

- `automatic`: the system chooses based on the symbol.
- `color`: fades inactive layers using opacity.
- `draw`: partially draws layers instead of fading them.

- Type: `'automatic' | 'color' | 'draw'`
- Default: `'automatic'`

Ignored on earlier versions.

### `renderingMode`

How the symbol's layers are colored.

- `monochrome`: single color tint.
- `hierarchical`: a hierarchy derived from a single color.
- `palette`: explicit colors per layer, taken from `colors`.
- `multicolor`: the symbol's built-in multicolor scheme.

- Type: `'monochrome' | 'hierarchical' | 'palette' | 'multicolor'`
- Default: `'monochrome'`

Anything other than `monochrome` opts the icon out of `tabBarActiveTintColor` and `tabBarInactiveTintColor`, since the symbol then carries colors of its own.

```tsx
focusedIcon: {
sfSymbol: 'person.crop.circle.badge.plus',
renderingMode: 'hierarchical',
color: '#AF52DE',
}
```

### `colors`

Colors used by the non-monochrome rendering modes.

- `hierarchical` uses `primary` as the base color.
- `palette` uses `primary`, `secondary` and `tertiary` for each layer.
- `multicolor` ignores them.

Falls back to `color` for `primary` when unset.

- Type: `{ primary?: ColorValue; secondary?: ColorValue; tertiary?: ColorValue }`

```tsx
focusedIcon: {
sfSymbol: 'square.grid.3x2.fill',
renderingMode: 'palette',
colors: { primary: '#FF3B30', secondary: '#34C759' },
}
```

### `colorRenderingMode` <Badge text="iOS 26+" type="info" />

How color is applied across the symbol's layers.

- `automatic`: the system chooses based on the symbol.
- `flat`: a solid color per layer.
- `gradient`: a gradient derived from each layer's color.

- Type: `'automatic' | 'flat' | 'gradient'`
- Default: `'automatic'`

Ignored on earlier versions.

## Configuring focused and unfocused states separately

Each state takes its own icon object, so the options can differ between them.

```tsx
{
key: 'home',
title: 'Home',
focusedIcon: { sfSymbol: 'house.fill', weight: 'bold' },
unfocusedIcon: { sfSymbol: 'house', weight: 'light' },
}
```

With React Navigation, return a different object per state from `tabBarIcon`:

```tsx
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarIcon: ({ focused }) =>
focused
? { sfSymbol: 'house.fill', weight: 'bold' }
: { sfSymbol: 'house', weight: 'light' },
}}
/>
```

## Unsupported options

React Navigation's `SFSymbol` component also accepts `effect` and `contentTransition`, which animate a symbol as it changes. Neither applies here: a tab bar item renders a still image, and the system never runs symbol animations on one. Both are omitted rather than silently ignored.

`style` is likewise absent, since a tab item is laid out by the tab bar rather than by your styles.
2 changes: 1 addition & 1 deletion docs/docs/docs/guides/standalone-usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ Each route in the `routes` array can have the following properties:

- `key`: Unique identifier for the route
- `title`: Display title for the tab
- `focusedIcon`: Icon to show when tab is active
- `focusedIcon`: Icon to show when tab is active. Either an image or an SF Symbol, which takes its own [configuration options](/docs/guides/sf-symbols)
- `unfocusedIcon`: Icon to show when tab is inactive (optional)
- `iconRenderingMode`: Rendering mode for icons. Use `'original'` to preserve multicolor icons instead of applying the native tab tint.
- `badge`: Badge text to display on the tab
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/docs/guides/usage-with-expo-router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export default function TabLayout() {

> [!NOTE] SF Symbols are only available on Apple platforms. On Android and web, pass an image with `require()` or a `{ uri }` object instead.

SF Symbols accept further configuration, such as `weight`, `scale`, `renderingMode` and per-layer `colors`. See the [SF Symbols guide](/docs/guides/sf-symbols) for every supported option.

For props and more information, see the [React Navigation integration guide](/docs/guides/usage-with-react-navigation), which documents every accepted `tabBarIcon` source.

Example: [okwasniewski/ExpoNativeTabs](https://github.com/okwasniewski/ExpoNativeTabs)
Expand Down
13 changes: 13 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
/>
```

SF Symbols accept further configuration, such as `weight`, `scale`, `renderingMode` and per-layer `colors`:

```tsx
tabBarIcon: () => ({
sfSymbol: 'person',
weight: 'semibold',
renderingMode: 'hierarchical',
color: '#AF52DE',
}),
```

See the [SF Symbols guide](/docs/guides/sf-symbols) for every supported option.

:::note
SF Symbols are only supported on Apple platforms.
:::
Expand Down
33 changes: 28 additions & 5 deletions packages/example-shared/src/Examples/SFSymbols.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { useState } from 'react';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';
import { Platform } from 'react-native';

const renderScene = SceneMap({
article: Article,
albums: Albums,
contacts: Contacts,
chat: Chat,
});

const isAndroid = Platform.OS === 'android';
Expand All @@ -21,27 +23,48 @@ export default function SFSymbols() {
title: 'Article',
focusedIcon: isAndroid
? require('../../assets/icons/article_dark.png')
: { sfSymbol: 'document.fill' },
: { sfSymbol: 'document.fill' as const, weight: 'bold' as const },
unfocusedIcon: isAndroid
? require('../../assets/icons/chat_dark.png')
: { sfSymbol: 'document' },
: { sfSymbol: 'document' as const, weight: 'light' as const },
badge: '!',
},
{
key: 'albums',
title: 'Albums',
// A palette symbol keeps its own per-layer colors instead of the tab tint.
focusedIcon: isAndroid
? require('../../assets/icons/grid_dark.png')
: { sfSymbol: 'square.grid.3x2.fill' },
: {
sfSymbol: 'square.grid.3x2.fill' as const,
renderingMode: 'palette' as const,
colors: { primary: '#FF3B30', secondary: '#34C759' },
},
badge: '5',
},
{
key: 'contacts',
focusedIcon: isAndroid
? require('../../assets/icons/person_dark.png')
: { sfSymbol: 'person.fill' },
: {
sfSymbol: 'person.fill' as const,
renderingMode: 'hierarchical' as const,
color: '#AF52DE',
},
title: 'Contacts',
role: 'search',
role: 'search' as const,
},
{
key: 'chat',
title: 'Signal',
// A variable symbol rendered at 60% of its layers.
focusedIcon: isAndroid
? require('../../assets/icons/chat_dark.png')
: {
sfSymbol: 'wifi' as const,
variableValue: 0.6,
scale: 'large' as const,
},
},
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ FOUNDATION_EXPORT NSObject *RNCCreateBottomAccessoryProvider(id<RNCBottomAccesso
title:(NSString *)title
badge:(NSString *)badge
sfSymbol:(NSString *)sfSymbol
sfSymbolOptions:(NSDictionary *)sfSymbolOptions
focusedSfSymbol:(NSString *)focusedSfSymbol
focusedSfSymbolOptions:(NSDictionary *)focusedSfSymbolOptions
activeTintColor:(UIColor *)activeTintColor
iconRenderingMode:(NSString *)iconRenderingMode
hidden:(BOOL)hidden
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ + (NSObject *)createWithKey:(NSString *)key
title:(NSString *)title
badge:(NSString *)badge
sfSymbol:(NSString *)sfSymbol
sfSymbolOptions:(NSDictionary *)sfSymbolOptions
focusedSfSymbol:(NSString *)focusedSfSymbol
focusedSfSymbolOptions:(NSDictionary *)focusedSfSymbolOptions
activeTintColor:(UIColor *)activeTintColor
iconRenderingMode:(NSString *)iconRenderingMode
hidden:(BOOL)hidden
testID:(NSString *)testID
role:(NSString *)role
preventsDefault:(BOOL)preventsDefault {
return [[TabInfo alloc] initWithKey:key title:title badge:badge sfSymbol:sfSymbol
focusedSfSymbol:focusedSfSymbol activeTintColor:activeTintColor
sfSymbolOptions:sfSymbolOptions
focusedSfSymbol:focusedSfSymbol
focusedSfSymbolOptions:focusedSfSymbolOptions
activeTintColor:activeTintColor
iconRenderingMode:iconRenderingMode hidden:hidden testID:testID
role:role preventsDefault:preventsDefault];
}
Expand Down
Loading