diff --git a/.changeset/sf-symbol-configuration.md b/.changeset/sf-symbol-configuration.md
new file mode 100644
index 00000000..4f643f75
--- /dev/null
+++ b/.changeset/sf-symbol-configuration.md
@@ -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.
diff --git a/docs/docs/docs/guides/_meta.json b/docs/docs/docs/guides/_meta.json
index fa7dd987..23638eef 100644
--- a/docs/docs/docs/guides/_meta.json
+++ b/docs/docs/docs/guides/_meta.json
@@ -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"]
diff --git a/docs/docs/docs/guides/sf-symbols.mdx b/docs/docs/docs/guides/sf-symbols.mdx
new file mode 100644
index 00000000..98702413
--- /dev/null
+++ b/docs/docs/docs/guides/sf-symbols.mdx
@@ -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`
+
+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`
+
+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
+
+ 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.
diff --git a/docs/docs/docs/guides/standalone-usage.mdx b/docs/docs/docs/guides/standalone-usage.mdx
index d9f02c43..ef63043a 100644
--- a/docs/docs/docs/guides/standalone-usage.mdx
+++ b/docs/docs/docs/guides/standalone-usage.mdx
@@ -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
diff --git a/docs/docs/docs/guides/usage-with-expo-router.mdx b/docs/docs/docs/guides/usage-with-expo-router.mdx
index 111025c3..3e4f78bc 100644
--- a/docs/docs/docs/guides/usage-with-expo-router.mdx
+++ b/docs/docs/docs/guides/usage-with-expo-router.mdx
@@ -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)
diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx
index 78e49d55..643a0c61 100644
--- a/docs/docs/docs/guides/usage-with-react-navigation.mdx
+++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx
@@ -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.
:::
diff --git a/packages/example-shared/src/Examples/SFSymbols.tsx b/packages/example-shared/src/Examples/SFSymbols.tsx
index f9490fbb..0c646e9d 100644
--- a/packages/example-shared/src/Examples/SFSymbols.tsx
+++ b/packages/example-shared/src/Examples/SFSymbols.tsx
@@ -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';
@@ -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,
+ },
},
]);
diff --git a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h
index 798e2cf0..a8099524 100644
--- a/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h
+++ b/packages/react-native-bottom-tabs/ios/Bridge/BottomTabsBridge.h
@@ -51,7 +51,9 @@ FOUNDATION_EXPORT NSObject *RNCCreateBottomAccessoryProvider(id
+bool sfSymbolOptionsEqual(const SymbolOptions& lhs, const SymbolOptions& rhs) {
+ return lhs.size == rhs.size &&
+ lhs.weight == rhs.weight &&
+ lhs.scale == rhs.scale &&
+ lhs.color == rhs.color &&
+ lhs.primaryColor == rhs.primaryColor &&
+ lhs.secondaryColor == rhs.secondaryColor &&
+ lhs.tertiaryColor == rhs.tertiaryColor &&
+ lhs.renderingMode == rhs.renderingMode &&
+ lhs.variableValue == rhs.variableValue &&
+ lhs.variableValueMode == rhs.variableValueMode &&
+ lhs.colorRenderingMode == rhs.colorRenderingMode;
+}
+
bool operator==(const RNCTabViewItemsStruct& lhs, const RNCTabViewItemsStruct& rhs) {
return lhs.key == rhs.key &&
lhs.title == rhs.title &&
lhs.sfSymbol == rhs.sfSymbol &&
+ sfSymbolOptionsEqual(lhs.sfSymbolOptions, rhs.sfSymbolOptions) &&
lhs.focusedSfSymbol == rhs.focusedSfSymbol &&
+ sfSymbolOptionsEqual(lhs.focusedSfSymbolOptions, rhs.focusedSfSymbolOptions) &&
lhs.badge == rhs.badge &&
lhs.activeTintColor == rhs.activeTintColor &&
lhs.iconRenderingMode == rhs.iconRenderingMode &&
@@ -223,10 +243,73 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
[super updateProps:props oldProps:oldProps];
}
+// Converts the flattened SF Symbol options into a dictionary the Swift side
+// parses. Sentinel values (`0` for size and weight, `-1` for variableValue,
+// empty strings) mean "not configured" and are dropped, so Swift sees `nil` and
+// leaves the corresponding platform default alone.
+template
+static NSDictionary* convertSymbolOptions(const SymbolOptions& options) {
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+
+ if (options.size > 0) {
+ result[@"size"] = @(options.size);
+ }
+
+ if (options.weight > 0) {
+ result[@"weight"] = @(options.weight);
+ }
+
+ if (!options.scale.empty()) {
+ result[@"scale"] = RCTNSStringFromString(options.scale);
+ }
+
+ if (UIColor *color = RCTUIColorFromSharedColor(options.color)) {
+ result[@"color"] = color;
+ }
+
+ if (UIColor *color = RCTUIColorFromSharedColor(options.primaryColor)) {
+ result[@"primaryColor"] = color;
+ }
+
+ if (UIColor *color = RCTUIColorFromSharedColor(options.secondaryColor)) {
+ result[@"secondaryColor"] = color;
+ }
+
+ if (UIColor *color = RCTUIColorFromSharedColor(options.tertiaryColor)) {
+ result[@"tertiaryColor"] = color;
+ }
+
+ if (!options.renderingMode.empty()) {
+ result[@"renderingMode"] = RCTNSStringFromString(options.renderingMode);
+ }
+
+ if (options.variableValue >= 0) {
+ result[@"variableValue"] = @(options.variableValue);
+ }
+
+ if (!options.variableValueMode.empty()) {
+ result[@"variableValueMode"] = RCTNSStringFromString(options.variableValueMode);
+ }
+
+ if (!options.colorRenderingMode.empty()) {
+ result[@"colorRenderingMode"] = RCTNSStringFromString(options.colorRenderingMode);
+ }
+
+ return result.count > 0 ? result : nil;
+}
+
NSArray* convertItemsToArray(const std::vector& items) {
NSMutableArray *result = [NSMutableArray array];
for (const auto& item : items) {
+ // Options only ever apply alongside a symbol name, and the generated struct
+ // cannot be null, so an item without a symbol carries default values that
+ // must not be mistaken for configuration.
+ NSDictionary *symbolOptions =
+ item.sfSymbol.empty() ? nil : convertSymbolOptions(item.sfSymbolOptions);
+ NSDictionary *focusedSymbolOptions =
+ item.focusedSfSymbol.empty() ? nil : convertSymbolOptions(item.focusedSfSymbolOptions);
+
#if SWIFT_PACKAGE
auto tabInfo = [RNCTabInfo createWithKey:RCTNSStringFromString(item.key)
#else
@@ -235,7 +318,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
title:RCTNSStringFromString(item.title)
badge:RCTNSStringFromStringNilIfEmpty(item.badge)
sfSymbol:RCTNSStringFromStringNilIfEmpty(item.sfSymbol)
+ sfSymbolOptions:symbolOptions
focusedSfSymbol:RCTNSStringFromStringNilIfEmpty(item.focusedSfSymbol)
+ focusedSfSymbolOptions:focusedSymbolOptions
activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)
iconRenderingMode:RCTNSStringFromStringNilIfEmpty(item.iconRenderingMode)
hidden:item.hidden
diff --git a/packages/react-native-bottom-tabs/ios/SFSymbolOptions.swift b/packages/react-native-bottom-tabs/ios/SFSymbolOptions.swift
new file mode 100644
index 00000000..cc5d09c9
--- /dev/null
+++ b/packages/react-native-bottom-tabs/ios/SFSymbolOptions.swift
@@ -0,0 +1,439 @@
+import Foundation
+
+#if os(macOS)
+ import AppKit
+#else
+ import UIKit
+#endif
+
+/// SF Symbol configuration for a single tab item icon.
+///
+/// Mirrors the SF Symbol options React Navigation exposes, limited to the ones
+/// that describe a static symbol image. Symbol effects and content transitions
+/// are intentionally absent: a tab bar item renders a still image and never
+/// runs symbol animations.
+///
+/// Every value is optional. When one is `nil` the platform default is left
+/// untouched, so an icon without options renders exactly as it did before any
+/// of this configuration existed.
+@objcMembers
+public final class SFSymbolOptions: NSObject {
+ /// Point size of the symbol.
+ public var size: NSNumber?
+ /// Symbol weight as a numeric value between `100` and `900`.
+ public var weight: NSNumber?
+ /// `small`, `medium` or `large`.
+ public var scale: String?
+ /// Tint color, and the fallback for `primaryColor`.
+ public var color: PlatformColor?
+ /// Color of the first layer.
+ public var primaryColor: PlatformColor?
+ /// Color of the second layer, `palette` mode only.
+ public var secondaryColor: PlatformColor?
+ /// Color of the third layer, `palette` mode only.
+ public var tertiaryColor: PlatformColor?
+ /// `monochrome`, `hierarchical`, `palette` or `multicolor`.
+ public var renderingMode: String?
+ /// Value between `0` and `1` for variable symbols.
+ public var variableValue: NSNumber?
+ /// `automatic`, `color` or `draw`. iOS 26+.
+ public var variableValueMode: String?
+ /// `automatic`, `flat` or `gradient`. iOS 26+.
+ public var colorRenderingMode: String?
+
+ public override init() {
+ super.init()
+ }
+
+ /// Builds options from the dictionary the Fabric component view assembles.
+ ///
+ /// Returns `nil` for a missing dictionary so callers can keep the untouched
+ /// default rendering path.
+ public convenience init?(dictionary: NSDictionary?) {
+ guard let dictionary else { return nil }
+
+ self.init()
+
+ size = dictionary["size"] as? NSNumber
+ weight = dictionary["weight"] as? NSNumber
+ scale = dictionary["scale"] as? String
+ color = dictionary["color"] as? PlatformColor
+ primaryColor = dictionary["primaryColor"] as? PlatformColor
+ secondaryColor = dictionary["secondaryColor"] as? PlatformColor
+ tertiaryColor = dictionary["tertiaryColor"] as? PlatformColor
+ renderingMode = dictionary["renderingMode"] as? String
+ variableValue = dictionary["variableValue"] as? NSNumber
+ variableValueMode = dictionary["variableValueMode"] as? String
+ colorRenderingMode = dictionary["colorRenderingMode"] as? String
+ }
+}
+
+extension SFSymbolOptions {
+ /// The color the first layer should use, falling back to the plain tint.
+ var effectivePrimaryColor: PlatformColor? {
+ primaryColor ?? color
+ }
+
+ /// Whether any color at all was configured.
+ var hasExplicitColors: Bool {
+ color != nil || primaryColor != nil || secondaryColor != nil || tertiaryColor != nil
+ }
+
+ /// Whether the resulting image carries its own colors and must therefore
+ /// bypass the tab bar's active/inactive tint.
+ ///
+ /// A layered rendering mode without any color of its own stays tintable, so
+ /// picking `hierarchical` alone still follows the tab bar's active and
+ /// inactive colors rather than falling back to black.
+ var preservesOwnColors: Bool {
+ switch renderingMode {
+ case "multicolor":
+ return true
+ case "hierarchical", "palette":
+ return hasExplicitColors
+ default:
+ return effectivePrimaryColor != nil
+ }
+ }
+
+ /// Whether anything at all was configured.
+ var isEmpty: Bool {
+ size == nil
+ && weight == nil
+ && scale == nil
+ && color == nil
+ && primaryColor == nil
+ && secondaryColor == nil
+ && tertiaryColor == nil
+ && renderingMode == nil
+ && variableValue == nil
+ && variableValueMode == nil
+ && colorRenderingMode == nil
+ }
+
+ /// The variable value clamped to the `0...1` range the symbol APIs accept.
+ var clampedVariableValue: Double? {
+ guard let variableValue else { return nil }
+ return min(max(variableValue.doubleValue, 0), 1)
+ }
+}
+
+// MARK: - Image building
+
+#if os(macOS)
+ extension SFSymbolOptions {
+ /// Builds a configured symbol image, or `nil` when the symbol is unknown.
+ static func makeImage(named name: String, options: SFSymbolOptions?) -> NSImage? {
+ guard !name.isEmpty else { return nil }
+
+ var image: NSImage?
+
+ if #available(macOS 13.0, *), let variableValue = options?.clampedVariableValue {
+ image = NSImage(
+ systemSymbolName: name,
+ variableValue: variableValue,
+ accessibilityDescription: nil
+ )
+ }
+
+ if image == nil {
+ image = NSImage(systemSymbolName: name, accessibilityDescription: nil)
+ }
+
+ guard let image else { return nil }
+ guard let options, !options.isEmpty else { return image }
+ // `NSImage.SymbolConfiguration.applying(_:)` landed in macOS 12, and
+ // combining configurations is what makes the options composable. On
+ // macOS 11 the symbol renders with the platform defaults instead.
+ guard #available(macOS 12.0, *) else { return image }
+
+ guard let configuration = options.symbolConfiguration() else { return image }
+
+ let configured = image.withSymbolConfiguration(configuration) ?? image
+ // A template image is recolored by the tab bar, discarding the symbol's
+ // own colors, so a colored symbol has to opt out of templating.
+ configured.isTemplate = !options.preservesOwnColors
+
+ return configured
+ }
+
+ @available(macOS 12.0, *)
+ private func symbolConfiguration() -> NSImage.SymbolConfiguration? {
+ var configuration: NSImage.SymbolConfiguration?
+
+ func apply(_ next: NSImage.SymbolConfiguration) {
+ configuration = configuration?.applying(next) ?? next
+ }
+
+ if let size = size?.doubleValue, size > 0 {
+ apply(
+ NSImage.SymbolConfiguration(
+ pointSize: CGFloat(size),
+ weight: Self.symbolWeight(from: weight)
+ )
+ )
+ } else if weight != nil {
+ apply(NSImage.SymbolConfiguration(pointSize: NSFont.systemFontSize, weight: Self.symbolWeight(from: weight)))
+ }
+
+ if let scale, let symbolScale = Self.symbolScale(from: scale) {
+ apply(NSImage.SymbolConfiguration(scale: symbolScale))
+ }
+
+ switch renderingMode {
+ case "hierarchical":
+ if let primary = effectivePrimaryColor {
+ apply(NSImage.SymbolConfiguration(hierarchicalColor: primary))
+ }
+ case "palette":
+ let paletteColors = [effectivePrimaryColor, secondaryColor, tertiaryColor]
+ .compactMap { $0 }
+ if !paletteColors.isEmpty {
+ apply(NSImage.SymbolConfiguration(paletteColors: paletteColors))
+ }
+ case "multicolor":
+ apply(NSImage.SymbolConfiguration.preferringMulticolor())
+ default:
+ // A tab item cannot carry a tint of its own, so a monochrome symbol
+ // with an explicit color is colored through a single-color palette.
+ if let primary = effectivePrimaryColor {
+ apply(NSImage.SymbolConfiguration(paletteColors: [primary]))
+ }
+ }
+
+ return configuration
+ }
+
+ @available(macOS 12.0, *)
+ private static func symbolWeight(from value: NSNumber?) -> NSFont.Weight {
+ switch value?.intValue {
+ case 100: return .thin
+ case 200: return .ultraLight
+ case 300: return .light
+ case 400: return .regular
+ case 500: return .medium
+ case 600: return .semibold
+ case 700: return .bold
+ case 800: return .heavy
+ case 900: return .black
+ default: return .regular
+ }
+ }
+
+ @available(macOS 12.0, *)
+ private static func symbolScale(from value: String) -> NSImage.SymbolScale? {
+ switch value {
+ case "small": return .small
+ case "medium": return .medium
+ case "large": return .large
+ default: return nil
+ }
+ }
+ }
+#else
+ extension SFSymbolOptions {
+ /// Builds a configured symbol image, or `nil` when the symbol is unknown.
+ ///
+ /// Falls back to a symbol from the app's asset catalog, so custom symbols
+ /// shipped alongside SF Symbols keep working.
+ static func makeImage(named name: String, options: SFSymbolOptions?) -> UIImage? {
+ guard !name.isEmpty else { return nil }
+
+ let configuration = options?.symbolConfiguration()
+
+ guard
+ let image = baseImage(
+ named: name,
+ variableValue: options?.clampedVariableValue,
+ configuration: configuration
+ )
+ else { return nil }
+
+ guard let options else { return image }
+
+ return options.tinted(image)
+ }
+
+ private static func baseImage(
+ named name: String,
+ variableValue: Double?,
+ configuration: UIImage.SymbolConfiguration?
+ ) -> UIImage? {
+ if let variableValue, #available(iOS 16.0, tvOS 16.0, *) {
+ if let image = UIImage(
+ systemName: name,
+ variableValue: variableValue,
+ configuration: configuration
+ ) {
+ return image
+ }
+ }
+
+ guard let configuration else {
+ return UIImage(systemName: name) ?? UIImage(named: name)
+ }
+
+ return UIImage(systemName: name, withConfiguration: configuration)
+ ?? UIImage(named: name)?.applyingSymbolConfiguration(configuration)
+ }
+
+ /// Settles the final colors and rendering mode of the image.
+ ///
+ /// A tab bar templates whatever image it is handed, which throws away the
+ /// symbol's own colors, so a colored symbol has to opt out with
+ /// `.alwaysOriginal` the same way an `original` image icon does. A
+ /// monochrome symbol has no layer colors to keep, so its color is baked in
+ /// as a flat tint instead.
+ private func tinted(_ image: UIImage) -> UIImage {
+ guard preservesOwnColors else { return image }
+
+ switch renderingMode {
+ case "multicolor":
+ return Self.flattened(image)
+ case "hierarchical", "palette":
+ if #available(iOS 15.0, tvOS 15.0, *) {
+ return Self.flattened(image)
+ }
+ // Layered color APIs do not exist before iOS 15, so the primary color
+ // is applied as a flat tint as the closest approximation.
+ guard let color = effectivePrimaryColor else { return image }
+ return Self.flattened(image.withTintColor(color, renderingMode: .alwaysOriginal))
+ default:
+ guard let color = effectivePrimaryColor else { return image }
+ return Self.flattened(image.withTintColor(color, renderingMode: .alwaysOriginal))
+ }
+ }
+
+ /// Draws the symbol into a plain bitmap that keeps the colors it was
+ /// configured with.
+ ///
+ /// Colors have to survive two separate attempts to recolor them. A tab bar
+ /// templates the image it is handed, and SwiftUI applies its own
+ /// `symbolRenderingMode` to anything it recognises as a symbol, which
+ /// flattens the layered modes back to a single tint. Rasterizing drops the
+ /// symbol identity, so neither applies, and `.alwaysOriginal` keeps the
+ /// result untinted.
+ private static func flattened(_ image: UIImage) -> UIImage {
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = image.scale
+ format.opaque = false
+
+ let rendered = UIGraphicsImageRenderer(size: image.size, format: format).image { _ in
+ image.draw(in: CGRect(origin: .zero, size: image.size))
+ }
+
+ return rendered.withRenderingMode(.alwaysOriginal)
+ }
+
+ /// Translates the options into a `UIImage.SymbolConfiguration`.
+ ///
+ /// Returns `nil` when nothing was configured, so the caller can request an
+ /// unconfigured image and keep the tab bar's own sizing.
+ func symbolConfiguration() -> UIImage.SymbolConfiguration? {
+ guard !isEmpty else { return nil }
+
+ var configuration: UIImage.SymbolConfiguration?
+
+ func apply(_ next: UIImage.SymbolConfiguration) {
+ configuration = configuration?.applying(next) ?? next
+ }
+
+ if let size = size?.doubleValue, size > 0 {
+ apply(UIImage.SymbolConfiguration(pointSize: CGFloat(size)))
+ }
+
+ if let weight {
+ apply(UIImage.SymbolConfiguration(weight: Self.symbolWeight(from: weight)))
+ }
+
+ if let scale, let symbolScale = Self.symbolScale(from: scale) {
+ apply(UIImage.SymbolConfiguration(scale: symbolScale))
+ }
+
+ #if compiler(>=6.2)
+ if #available(iOS 26.0, tvOS 26.0, *) {
+ if let variableValueMode,
+ let mode = Self.symbolVariableValueMode(from: variableValueMode) {
+ apply(UIImage.SymbolConfiguration(variableValueMode: mode))
+ }
+
+ if let colorRenderingMode,
+ let mode = Self.symbolColorRenderingMode(from: colorRenderingMode) {
+ apply(UIImage.SymbolConfiguration(colorRenderingMode: mode))
+ }
+ }
+ #endif
+
+ switch renderingMode {
+ case "hierarchical":
+ if let primary = effectivePrimaryColor {
+ if #available(iOS 15.0, tvOS 15.0, *) {
+ apply(UIImage.SymbolConfiguration(hierarchicalColor: primary))
+ }
+ }
+ case "palette":
+ let paletteColors = [effectivePrimaryColor, secondaryColor, tertiaryColor]
+ .compactMap { $0 }
+ if !paletteColors.isEmpty, #available(iOS 15.0, tvOS 15.0, *) {
+ apply(UIImage.SymbolConfiguration(paletteColors: paletteColors))
+ }
+ case "multicolor":
+ if #available(iOS 15.0, tvOS 15.0, *) {
+ apply(UIImage.SymbolConfiguration.preferringMulticolor())
+ }
+ default:
+ break
+ }
+
+ return configuration
+ }
+
+ private static func symbolWeight(from value: NSNumber) -> UIImage.SymbolWeight {
+ switch value.intValue {
+ case 100: return .thin
+ case 200: return .ultraLight
+ case 300: return .light
+ case 400: return .regular
+ case 500: return .medium
+ case 600: return .semibold
+ case 700: return .bold
+ case 800: return .heavy
+ case 900: return .black
+ default: return .unspecified
+ }
+ }
+
+ private static func symbolScale(from value: String) -> UIImage.SymbolScale? {
+ switch value {
+ case "small": return .small
+ case "medium": return .medium
+ case "large": return .large
+ default: return nil
+ }
+ }
+
+ #if compiler(>=6.2)
+ @available(iOS 26.0, tvOS 26.0, *)
+ private static func symbolVariableValueMode(from value: String)
+ -> UIImage.SymbolVariableValueMode? {
+ switch value {
+ case "automatic": return .automatic
+ case "color": return .color
+ case "draw": return .draw
+ default: return nil
+ }
+ }
+
+ @available(iOS 26.0, tvOS 26.0, *)
+ private static func symbolColorRenderingMode(from value: String)
+ -> UIImage.SymbolColorRenderingMode? {
+ switch value {
+ case "automatic": return .automatic
+ case "flat": return .flat
+ case "gradient": return .gradient
+ default: return nil
+ }
+ }
+ #endif
+ }
+#endif
diff --git a/packages/react-native-bottom-tabs/ios/TabItem.swift b/packages/react-native-bottom-tabs/ios/TabItem.swift
index 3992f27d..b810e61e 100644
--- a/packages/react-native-bottom-tabs/ios/TabItem.swift
+++ b/packages/react-native-bottom-tabs/ios/TabItem.swift
@@ -4,6 +4,7 @@ struct TabItem: View {
var title: String?
var icon: PlatformImage?
var sfSymbol: String?
+ var sfSymbolOptions: SFSymbolOptions?
var labeled: Bool?
var iconRenderingMode: String?
@@ -15,14 +16,36 @@ struct TabItem: View {
Image(uiImage: renderedIcon(icon))
#endif
} else if let sfSymbol, !sfSymbol.isEmpty {
- Image(systemName: sfSymbol)
- .noneSymbolVariant()
+ symbolImage(sfSymbol)
}
if labeled != false {
Text(title ?? "")
}
}
+ /// Renders the SF Symbol.
+ ///
+ /// Without any configuration this stays on `Image(systemName:)` so the
+ /// symbol keeps the tab bar's own sizing and tinting. Once options are set,
+ /// the symbol is built as a configured image instead, and opts out of the
+ /// tab bar tint when it carries colors of its own.
+ @ViewBuilder
+ private func symbolImage(_ sfSymbol: String) -> some View {
+ if let sfSymbolOptions, !sfSymbolOptions.isEmpty,
+ let image = SFSymbolOptions.makeImage(named: sfSymbol, options: sfSymbolOptions) {
+ // The image already carries its own rendering mode, so a colored symbol
+ // keeps its colors here without any further SwiftUI modifier.
+#if os(macOS)
+ Image(nsImage: image)
+#else
+ Image(uiImage: image)
+#endif
+ } else {
+ Image(systemName: sfSymbol)
+ .noneSymbolVariant()
+ }
+ }
+
#if !os(macOS)
private var preservesOriginalIconColors: Bool {
iconRenderingMode == "original"
diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift
index 188ddfde..986c1c25 100644
--- a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift
+++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift
@@ -56,6 +56,7 @@ struct LegacyTabView: AnyTabView {
title: tabData.title,
icon: icon,
sfSymbol: tabData.sfSymbol,
+ sfSymbolOptions: tabData.sfSymbolOptions,
labeled: props.labeled,
iconRenderingMode: tabData.iconRenderingMode
)
diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
index 2f18f210..01477f08 100644
--- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
+++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
@@ -48,6 +48,7 @@ struct NewTabView: AnyTabView {
title: tabData.title,
icon: icon,
sfSymbol: tabData.sfSymbol,
+ sfSymbolOptions: tabData.sfSymbolOptions,
labeled: props.labeled,
iconRenderingMode: tabData.iconRenderingMode
)
diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
index 68053e1f..ce84015a 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
@@ -227,9 +227,15 @@ struct TabViewImpl: View {
let tabActiveColor = tabData.activeTintColor ?? props.activeTintColor
let assetIcon = props.icons[itemIndex]
- let icon = assetIcon ?? makeSFSymbolImage(named: tabData.sfSymbol)
+ let icon =
+ assetIcon
+ ?? makeSFSymbolImage(named: tabData.sfSymbol, options: tabData.sfSymbolOptions)
let focusedIcon =
- props.focusedIcons[itemIndex] ?? makeSFSymbolImage(named: tabData.focusedSfSymbol) ?? icon
+ props.focusedIcons[itemIndex]
+ ?? makeSFSymbolImage(
+ named: tabData.focusedSfSymbol,
+ options: tabData.focusedSfSymbolOptions
+ ) ?? icon
let preservesOriginalIconColors = preservesOriginalIconColors(tabData: tabData)
let useBakedTintColors = shouldUseExperimentalBakedTintColors(props: props)
let shouldRenderLabelIntoImage =
@@ -290,7 +296,15 @@ struct TabViewImpl: View {
}
private func preservesOriginalIconColors(tabData: TabInfo) -> Bool {
- tabData.iconRenderingMode == "original"
+ if tabData.iconRenderingMode == "original" {
+ return true
+ }
+
+ // A symbol configured with its own colors, or with a non-monochrome
+ // rendering mode, would be flattened by the tab bar tint. Treat it the
+ // same way as an image icon that opts out of tinting.
+ return tabData.sfSymbolOptions?.preservesOwnColors == true
+ || tabData.focusedSfSymbolOptions?.preservesOwnColors == true
}
private func renderTabBarIcon(
@@ -310,10 +324,10 @@ struct TabViewImpl: View {
return icon.withTintColor(color, renderingMode: .alwaysOriginal)
}
- private func makeSFSymbolImage(named sfSymbol: String?) -> UIImage? {
+ private func makeSFSymbolImage(named sfSymbol: String?, options: SFSymbolOptions?) -> UIImage? {
guard let sfSymbol, !sfSymbol.isEmpty else { return nil }
- return UIImage(systemName: sfSymbol)
+ return SFSymbolOptions.makeImage(named: sfSymbol, options: options)
}
private func selectedAttributes(props: TabViewProps) -> [NSAttributedString.Key: Any] {
diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
index e5663453..b19fa1eb 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
@@ -8,7 +8,9 @@ public final class TabInfo: NSObject {
public let title: String
public let badge: String?
public let sfSymbol: String
+ public let sfSymbolOptions: SFSymbolOptions?
public let focusedSfSymbol: String?
+ public let focusedSfSymbolOptions: SFSymbolOptions?
public let activeTintColor: PlatformColor?
public let iconRenderingMode: String?
public let hidden: Bool
@@ -21,7 +23,9 @@ public final class TabInfo: NSObject {
title: String,
badge: String?,
sfSymbol: String,
+ sfSymbolOptions: NSDictionary?,
focusedSfSymbol: String?,
+ focusedSfSymbolOptions: NSDictionary?,
activeTintColor: PlatformColor?,
iconRenderingMode: String?,
hidden: Bool,
@@ -33,7 +37,9 @@ public final class TabInfo: NSObject {
self.title = title
self.badge = badge
self.sfSymbol = sfSymbol
+ self.sfSymbolOptions = SFSymbolOptions(dictionary: sfSymbolOptions)
self.focusedSfSymbol = focusedSfSymbol
+ self.focusedSfSymbolOptions = SFSymbolOptions(dictionary: focusedSfSymbolOptions)
self.activeTintColor = activeTintColor
self.iconRenderingMode = iconRenderingMode
self.hidden = hidden
diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx
index 0acfb842..1409b43c 100644
--- a/packages/react-native-bottom-tabs/src/TabView.tsx
+++ b/packages/react-native-bottom-tabs/src/TabView.tsx
@@ -3,6 +3,7 @@ import type {
OnNativeLayout,
OnPageSelectedEventData,
OnTabBarMeasured,
+ SFSymbolOptions,
TabViewItems,
} from './TabViewNativeComponent';
import {
@@ -36,8 +37,56 @@ import {
type BottomAccessoryViewProps,
} from './BottomAccessoryView';
-const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
- icon?.sfSymbol;
+const isAppleSymbol = (icon: any): icon is AppleIcon => icon?.sfSymbol;
+
+const SF_SYMBOL_WEIGHTS = {
+ thin: 100,
+ ultralight: 200,
+ light: 300,
+ regular: 400,
+ medium: 500,
+ semibold: 600,
+ bold: 700,
+ extrabold: 800,
+ black: 900,
+} as const;
+
+/**
+ * `variableValue` accepts `0`, so it needs a sentinel outside its `0...1`
+ * range to mean "not configured".
+ */
+const UNSET_VARIABLE_VALUE = -1;
+
+/**
+ * Flattens an `AppleIcon` into the scalar fields the native side reads.
+ *
+ * Values left undefined by the user are sent as sentinels so native keeps
+ * using the platform default instead of overriding it with a zero.
+ */
+const createSfSymbolOptions = (
+ icon: AppleIcon | undefined
+): SFSymbolOptions | undefined => {
+ if (!icon) {
+ return undefined;
+ }
+
+ return {
+ size: icon.size ?? 0,
+ weight:
+ typeof icon.weight === 'string'
+ ? SF_SYMBOL_WEIGHTS[icon.weight]
+ : (icon.weight ?? 0),
+ scale: icon.scale ?? '',
+ color: processColor(icon.color),
+ primaryColor: processColor(icon.colors?.primary),
+ secondaryColor: processColor(icon.colors?.secondary),
+ tertiaryColor: processColor(icon.colors?.tertiary),
+ renderingMode: icon.renderingMode ?? '',
+ variableValue: icon.variableValue ?? UNSET_VARIABLE_VALUE,
+ variableValueMode: icon.variableValueMode ?? '',
+ colorRenderingMode: icon.colorRenderingMode ?? '',
+ };
+};
interface Props {
/*
@@ -352,7 +401,11 @@ const TabView = ({
key: route.key,
title: getLabelText({ route }) ?? route.key,
sfSymbol: isSfSymbol ? icon.sfSymbol : undefined,
+ sfSymbolOptions: isSfSymbol ? createSfSymbolOptions(icon) : undefined,
focusedSfSymbol: isFocusedSfSymbol ? focusedIcon.sfSymbol : undefined,
+ focusedSfSymbolOptions: isFocusedSfSymbol
+ ? createSfSymbolOptions(focusedIcon)
+ : undefined,
badge: getBadge?.({ route }),
badgeBackgroundColor: processColor(
getBadgeBackgroundColor?.({ route })
diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
index 50ecf7c4..0969a574 100644
--- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
+++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
@@ -22,11 +22,34 @@ export type OnNativeLayout = Readonly<{
height: Double;
}>;
+/**
+ * Flattened SF Symbol configuration sent to the native side.
+ *
+ * Unset values are encoded with sentinels rather than omitted, so the native
+ * side can tell "not configured" apart from a legitimate zero:
+ * `0` for `size` and `weight`, `-1` for `variableValue`, `''` for strings.
+ */
+export type SFSymbolOptions = Readonly<{
+ size: Double;
+ weight: Int32;
+ scale: string;
+ color?: ProcessedColorValue | null;
+ primaryColor?: ProcessedColorValue | null;
+ secondaryColor?: ProcessedColorValue | null;
+ tertiaryColor?: ProcessedColorValue | null;
+ renderingMode: string;
+ variableValue: Double;
+ variableValueMode: string;
+ colorRenderingMode: string;
+}>;
+
export type TabViewItems = ReadonlyArray<{
key: string;
title: string;
sfSymbol?: string;
+ sfSymbolOptions?: SFSymbolOptions;
focusedSfSymbol?: string;
+ focusedSfSymbolOptions?: SFSymbolOptions;
badge?: string;
badgeBackgroundColor?: ProcessedColorValue | null;
badgeTextColor?: ProcessedColorValue | null;
diff --git a/packages/react-native-bottom-tabs/src/index.tsx b/packages/react-native-bottom-tabs/src/index.tsx
index 19544834..90a8d51e 100644
--- a/packages/react-native-bottom-tabs/src/index.tsx
+++ b/packages/react-native-bottom-tabs/src/index.tsx
@@ -19,5 +19,11 @@ export type {
AppleIcon,
IconRenderingMode,
LayoutDirection,
+ SFSymbolColorRenderingMode,
+ SFSymbolColors,
+ SFSymbolRenderingMode,
+ SFSymbolScale,
+ SFSymbolVariableValueMode,
+ SFSymbolWeight,
TabRole,
} from './types';
diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts
index 3f4eda80..2c4ee19d 100644
--- a/packages/react-native-bottom-tabs/src/types.ts
+++ b/packages/react-native-bottom-tabs/src/types.ts
@@ -1,9 +1,167 @@
-import type { ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
+import type {
+ ColorValue,
+ ImageSourcePropType,
+ StyleProp,
+ ViewStyle,
+} from 'react-native';
import type { SFSymbol } from 'sf-symbols-typescript';
export type IconSource = string | ImageSourcePropType;
-export type AppleIcon = { sfSymbol: SFSymbol };
+/**
+ * Weight of an SF Symbol. Accepts either a name or its numeric equivalent.
+ */
+export type SFSymbolWeight =
+ | 'thin'
+ | 'ultralight'
+ | 'light'
+ | 'regular'
+ | 'medium'
+ | 'semibold'
+ | 'bold'
+ | 'extrabold'
+ | 'black'
+ | 100
+ | 200
+ | 300
+ | 400
+ | 500
+ | 600
+ | 700
+ | 800
+ | 900;
+
+/**
+ * Scale variant of an SF Symbol.
+ */
+export type SFSymbolScale = 'small' | 'medium' | 'large';
+
+/**
+ * Rendering mode of an SF Symbol.
+ */
+export type SFSymbolRenderingMode =
+ | 'monochrome'
+ | 'hierarchical'
+ | 'palette'
+ | 'multicolor';
+
+/**
+ * How the partial state described by `variableValue` is rendered.
+ */
+export type SFSymbolVariableValueMode = 'automatic' | 'color' | 'draw';
+
+/**
+ * How color is applied across the layers of an SF Symbol.
+ */
+export type SFSymbolColorRenderingMode = 'automatic' | 'flat' | 'gradient';
+
+/**
+ * Per-layer colors of an SF Symbol.
+ */
+export type SFSymbolColors = {
+ primary?: ColorValue;
+ secondary?: ColorValue;
+ tertiary?: ColorValue;
+};
+
+/**
+ * An SF Symbol icon and its configuration.
+ *
+ * Only `sfSymbol` is required. Every other option falls back to the system
+ * default for a tab bar item, which is what you want in most cases.
+ *
+ * @platform ios, macOS, tvOS, visionOS
+ */
+export type AppleIcon = {
+ /**
+ * The name of the SF Symbol to display, e.g. `house.fill`.
+ */
+ sfSymbol: SFSymbol;
+ /**
+ * Point size of the symbol.
+ *
+ * Defaults to the size the tab bar picks for the current platform.
+ */
+ size?: number;
+ /**
+ * 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 the tab bar's active/inactive tint colors.
+ */
+ color?: ColorValue;
+ /**
+ * Weight of the symbol.
+ *
+ * @default 'regular'
+ */
+ weight?: SFSymbolWeight;
+ /**
+ * Scale variant of the symbol.
+ *
+ * @default 'medium'
+ */
+ scale?: SFSymbolScale;
+ /**
+ * Value used to customize variable symbols, between `0` and `1`.
+ *
+ * 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. Has no effect on non-variable symbols.
+ *
+ * Requires iOS 16+.
+ */
+ variableValue?: number;
+ /**
+ * 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.
+ *
+ * Requires iOS 26+. Ignored on earlier versions.
+ *
+ * @default 'automatic'
+ */
+ variableValueMode?: SFSymbolVariableValueMode;
+ /**
+ * Rendering mode of the symbol.
+ *
+ * - `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.
+ *
+ * Anything other than `monochrome` opts the icon out of the tab bar's
+ * active/inactive tint colors.
+ *
+ * @default 'monochrome'
+ */
+ renderingMode?: SFSymbolRenderingMode;
+ /**
+ * Colors used by the non-monochrome rendering modes.
+ *
+ * - `hierarchical`: uses `primary` as the base color.
+ * - `palette`: uses `primary`, `secondary` and `tertiary` per layer.
+ * - `multicolor`: ignored.
+ *
+ * Falls back to `color` for `primary` when unset.
+ */
+ colors?: SFSymbolColors;
+ /**
+ * 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.
+ *
+ * Requires iOS 26+. Ignored on earlier versions.
+ *
+ * @default 'automatic'
+ */
+ colorRenderingMode?: SFSymbolColorRenderingMode;
+};
export type TabRole = 'search';