shadimage
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>
  );
};

More from tetra-ui

Browse all components in this library →