tetra-ui
uisegmented-controlsource: ok · hit
Segmented Control
A native single-select control for choosing between a small set of options.
npx shadcn add "https://tetra-ui.com/r/segmented-control.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@expo/ui
Files6 · source cached 10/8/2026, 1:43:46 PM
ui/segmented-control/index.ts
export * from "./segmented-control";
export type {
SegmentedControlItemLabelProps,
SegmentedControlItemProps,
SegmentedControlProps,
} from "./segmented-control-types";
ui/segmented-control/segmented-control-types.ts
export type SegmentedControlProps = {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
disabled?: boolean;
className?: string;
children: React.ReactNode;
};
export type SegmentedControlItemProps = {
value: string;
children: React.ReactElement | React.ReactElement[];
};
export type SegmentedControlItemLabelProps = {
children: string;
};
export type SegmentedControlItemData = {
value: string;
label: string;
};
ui/segmented-control/segmented-control-utils.ts
import { Children, isValidElement } from "react";
import type {
SegmentedControlItemData,
SegmentedControlItemLabelProps,
SegmentedControlItemProps,
} from "./segmented-control-types";
const getChildDisplayName = (child: React.ReactNode) => {
if (!isValidElement(child)) {
return;
}
return (child.type as { displayName?: string }).displayName;
};
const extractItemLabel = (children: React.ReactNode) => {
let label: string | undefined;
for (const child of Children.toArray(children)) {
if (!isValidElement(child)) {
continue;
}
if (getChildDisplayName(child) === "SegmentedControlItemLabel") {
label = (child.props as SegmentedControlItemLabelProps).children;
}
}
return label;
};
export const extractSegmentedControlItems = (children: React.ReactNode) => {
const items: SegmentedControlItemData[] = [];
for (const child of Children.toArray(children)) {
if (!isValidElement(child)) {
continue;
}
if (getChildDisplayName(child) !== "SegmentedControlItem") {
continue;
}
const { value, children: itemChildren } =
child.props as SegmentedControlItemProps;
const label = extractItemLabel(itemChildren);
if (typeof value !== "string" || label === undefined) {
continue;
}
items.push({ label, value });
}
return items;
};
ui/segmented-control/segmented-control.tsx
import type {
SegmentedControlItemLabelProps,
SegmentedControlItemProps,
SegmentedControlProps,
} from "./segmented-control-types";
export const SegmentedControlItemLabel = (
_: SegmentedControlItemLabelProps
) => {
return null;
};
SegmentedControlItemLabel.displayName = "SegmentedControlItemLabel";
export const SegmentedControlItem = (_: SegmentedControlItemProps) => {
return null;
};
SegmentedControlItem.displayName = "SegmentedControlItem";
export const SegmentedControl = (_: SegmentedControlProps) => {
return null;
};
ui/segmented-control/segmented-control.ios.tsx
import { Host as HostPrimitive } from "@expo/ui";
import { Picker, Text } from "@expo/ui/swift-ui";
import {
disabled as disabledModifier,
type ModifierConfig,
pickerStyle,
tag,
tint,
} from "@expo/ui/swift-ui/modifiers";
import { useMemo, useState } from "react";
import { useCSSVariable, withUniwind } from "uniwind";
import { cn } from "@/lib/utils";
import type {
SegmentedControlItemLabelProps,
SegmentedControlItemProps,
SegmentedControlProps,
} from "./segmented-control-types";
import { extractSegmentedControlItems } from "./segmented-control-utils";
const StyledHost = withUniwind(HostPrimitive);
export const SegmentedControlItemLabel = (
_: SegmentedControlItemLabelProps
) => {
return null;
};
SegmentedControlItemLabel.displayName = "SegmentedControlItemLabel";
export const SegmentedControlItem = (_: SegmentedControlItemProps) => {
return null;
};
SegmentedControlItem.displayName = "SegmentedControlItem";
export const SegmentedControl = ({
value: valueProp,
defaultValue,
onValueChange,
disabled = false,
className,
children,
}: SegmentedControlProps) => {
const primaryColor = useCSSVariable("--color-primary") as string;
const items = extractSegmentedControlItems(children);
const [internalValue, setInternalValue] = useState(
defaultValue ?? items.at(0)?.value ?? ""
);
const isControlled = valueProp !== undefined;
const value = isControlled ? valueProp : internalValue;
const handleValueChange = (nextValue: string) => {
if (!isControlled) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
};
const modifiers = useMemo(() => {
const next: ModifierConfig[] = [
pickerStyle("segmented"),
tint(primaryColor),
];
if (disabled) {
next.push(disabledModifier(true));
}
return next;
}, [disabled, primaryColor]);
return (
<StyledHost className={cn("h-7.5 w-full", className)}>
<Picker
modifiers={modifiers}
onSelectionChange={(selection) => {
handleValueChange(String(selection));
}}
selection={value}
>
{items.map((item) => (
<Text key={item.value} modifiers={[tag(item.value)]}>
{item.label}
</Text>
))}
</Picker>
</StyledHost>
);
};
ui/segmented-control/segmented-control.android.tsx
import {
Host as HostPrimitive,
SegmentedButton,
SingleChoiceSegmentedButtonRow,
Text,
} from "@expo/ui/jetpack-compose";
import { height } from "@expo/ui/jetpack-compose/modifiers";
import { useState } from "react";
import { useCSSVariable, withUniwind } from "uniwind";
import { cn } from "@/lib/utils";
import type {
SegmentedControlItemLabelProps,
SegmentedControlItemProps,
SegmentedControlProps,
} from "./segmented-control-types";
import { extractSegmentedControlItems } from "./segmented-control-utils";
const StyledHost = withUniwind(HostPrimitive);
export const SegmentedControlItemLabel = (
_: SegmentedControlItemLabelProps
) => {
return null;
};
SegmentedControlItemLabel.displayName = "SegmentedControlItemLabel";
export const SegmentedControlItem = (_: SegmentedControlItemProps) => {
return null;
};
SegmentedControlItem.displayName = "SegmentedControlItem";
export const SegmentedControl = ({
value: valueProp,
defaultValue,
onValueChange,
disabled = false,
className,
children,
}: SegmentedControlProps) => {
const primaryColor = useCSSVariable("--color-primary") as string;
const primaryForegroundColor = useCSSVariable(
"--color-primary-foreground"
) as string;
const foregroundColor = useCSSVariable("--color-foreground") as string;
const mutedForegroundColor = useCSSVariable(
"--color-muted-foreground"
) as string;
const items = extractSegmentedControlItems(children);
const [internalValue, setInternalValue] = useState(
defaultValue ?? items.at(0)?.value ?? ""
);
const isControlled = valueProp !== undefined;
const value = isControlled ? valueProp : internalValue;
const handleValueChange = (nextValue: string) => {
if (!isControlled) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
};
return (
<StyledHost className={cn("h-8 w-full", className)}>
<SingleChoiceSegmentedButtonRow>
{items.map((item) => (
<SegmentedButton
colors={{
activeContainerColor: primaryColor,
activeContentColor: primaryForegroundColor,
disabledActiveContainerColor: mutedForegroundColor,
disabledActiveContentColor: primaryForegroundColor,
disabledInactiveContentColor: mutedForegroundColor,
inactiveContentColor: foregroundColor,
}}
enabled={!disabled}
key={item.value}
modifiers={[height(32)]}
onClick={() => {
handleValueChange(item.value);
}}
selected={item.value === value}
>
<SegmentedButton.Label>
<Text>{item.label}</Text>
</SegmentedButton.Label>
</SegmentedButton>
))}
</SingleChoiceSegmentedButtonRow>
</StyledHost>
);
};