One icon component that draws an SF Symbol on iOS and a Material Symbol on Android, colored by role
One.UI.Icon shows the right icon on each platform and colors it with the
platform’s own system colors. You give it an SF Symbol for iOS, a Material
Symbol for Android, and any element for web; it renders the one for the
current platform.
Color it by role, such as secondary or danger, and the icon follows the
system palette, light and dark mode included, with no React re-render.
import { One } from 'one'
const share = { ios: <One.iOS.Image systemName="square.and.arrow.up" accessibilityLabel="Share" />, android: <One.Android.Icon name="share" accessibilityLabel="Share" />,}
function ShareIcon() { return <One.UI.Icon icons={share} colorRole="accent" />}Keep the icons object at module scope and reuse it. A label makes the icon
readable by VoiceOver and TalkBack; without one it is decoration.
The component picks the element for the platform and clones it with color and size added. It renders nothing of its own.
One.iOS.Image element, which draws the symbol with
SwiftUI’s Image(systemName:). The role becomes the image’s foreground
style, a UIKit dynamic color, so it changes with the color scheme inside
SwiftUI. The size comes from swiftStyle.fontSize, 24 points by default;
the same width and height are given to both React Native layout and SwiftUI,
so the icon measures the same in both.One.Android.Icon element, a Compose Icon drawing
a Material Symbol. The role resolves against the current MaterialTheme
color scheme, which follows dark mode and, on Android 12 and later, the
wallpaper’s dynamic colors. The size comes from
size, 24 dp by default.icons.web unchanged, or nothing when it is missing.Passing color instead of colorRole sets the foreground color directly.
| One.UI.Icon | Type | Description |
|---|---|---|
icons (required)IconElementsThe element for each platform. | IconElements | The element for each platform. |
colorRoleIconColorRole = "primary"A system color by role. Not with color. | IconColorRole = "primary" | A system color by role. Not with color. |
colorColorValueA fixed color. Not with colorRole. | ColorValue | A fixed color. Not with colorRole. |
| IconElements | Type | Description |
|---|---|---|
iosReactElement<ImageProps>A One.iOS.Image element. Anything else throws on iOS. | ReactElement<ImageProps> | A One.iOS.Image element. Anything else throws on iOS. |
androidReactElement<ComposeIconProps>A One.Android.Icon element. Anything else throws on Android. | ReactElement<ComposeIconProps> | A One.Android.Icon element. Anything else throws on Android. |
webReactElementRendered as is on web. Without it, web renders nothing. | ReactElement | Rendered as is on web. Without it, web renders nothing. |
Each role maps to these colors:
| Role | iOS | Android |
|---|---|---|
| primary | label | onSurface |
| secondary | secondaryLabel | onSurfaceVariant |
| tertiary | tertiaryLabel | onSurfaceVariant at 60% |
| accent | The app accent color | primary |
| danger | systemRed | error |
| iOS | Android | Web | |
|---|---|---|---|
| Element | SwiftUI Image | Compose Icon | Your element |
| Glyphs | SF Symbols | Material Symbols | Your own |
| Default size | 24 points | 24 dp | Your element’s |
| Color roles | UIKit system colors | Material theme colors | Not applied |
ios and
android, and each platform throws if its element is not the expected
component.swiftStyle. swiftStyle.fontSize sets the symbol
size and the frame; swiftStyle.width and height, or numeric
style.width and height, override one side.color exclude each other. The type allows one or the other.
With color, no role is sent.import { Pressable, Text } from 'react-native'import { One } from 'one'
const trash = { ios: <One.iOS.Image systemName="trash" swiftStyle={{ fontSize: 18 }} />, android: <One.Android.Icon name="delete" size={18} />,}
function DeleteRow({ onDelete }: { onDelete: () => void }) { return ( <Pressable onPress={onDelete} accessibilityRole="button" accessibilityLabel="Delete" style={{ flexDirection: 'row', gap: 8 }}> <One.UI.Icon icons={trash} colorRole="danger" /> <Text>Delete</Text> </Pressable> )}The label is on the button, so the icon stays unlabeled and VoiceOver reads “Delete” once.
import { One } from 'one'
const heart = { ios: <One.iOS.Image systemName="heart.fill" accessibilityLabel="Liked" />, android: <One.Android.Icon name="favorite" accessibilityLabel="Liked" />,}
function Liked() { return <One.UI.Icon icons={heart} color="#E5484D" />}A fixed color skips the system palette, so the heart is the same red in
light and dark mode.
Edit this page on GitHub.