# Settings Device

A phone-style Settings screen with a search field, an account row, and grouped rows with switches.

Settings Device is an open-source block from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/settings-device

**React**

**Example**

```tsx title="components/blocks/settings-device.tsx"
"use client"

import {
  AirplaneIcon,
  BellIcon,
  BluetoothIcon,
  CaretRightIcon,
  CellSignalFullIcon,
  GearIcon,
  HourglassIcon,
  MagnifyingGlassIcon,
  MoonIcon,
  SpeakerHighIcon,
  SunIcon,
  WifiHighIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Switch } from "@/components/ui/switch"

type Toggles = {
  airplane: boolean
  wifi: boolean
  bluetooth: boolean
  focus: boolean
}

type Row = {
  label: string
  icon: React.ReactNode
  tint: string
  toggle?: keyof Toggles
  detail?: (toggles: Toggles) => string
}

const groups: Row[][] = [
  [
    {
      label: "Airplane Mode",
      icon: (
        <AirplaneIcon className="size-4.25" />
      ),
      tint: "bg-orange-500",
      toggle: "airplane",
    },
    {
      label: "Wi-Fi",
      icon: (
        <WifiHighIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.wifi ? "Home" : "Off"),
    },
    {
      label: "Bluetooth",
      icon: (
        <BluetoothIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.bluetooth ? "On" : "Off"),
    },
    {
      label: "Cellular",
      icon: (
        <CellSignalFullIcon className="size-4.25" />
      ),
      tint: "bg-green-500",
      detail: (toggles) => (toggles.airplane ? "Off" : ""),
    },
  ],
  [
    {
      label: "Notifications",
      icon: (
        <BellIcon className="size-4.25" />
      ),
      tint: "bg-red-500",
    },
    {
      label: "Sounds & Haptics",
      icon: (
        <SpeakerHighIcon className="size-4.25" />
      ),
      tint: "bg-pink-500",
    },
    {
      label: "Focus",
      icon: (
        <MoonIcon className="size-4.25" />
      ),
      tint: "bg-indigo-500",
      toggle: "focus",
    },
    {
      label: "Screen Time",
      icon: (
        <HourglassIcon className="size-4.25" />
      ),
      tint: "bg-indigo-500",
    },
  ],
  [
    {
      label: "General",
      icon: (
        <GearIcon className="size-4.25" />
      ),
      tint: "bg-zinc-500",
    },
    {
      label: "Display & Brightness",
      icon: (
        <SunIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
    },
  ],
]

function RowLink(props: React.ComponentProps<"a">) {
  return <a {...props} />
}

const groupClass = "shrink-0 bg-background dark:bg-muted"

export function SettingsDevice() {
  const idPrefix = React.useId()
  const [query, setQuery] = React.useState("")
  const [toggles, setToggles] = React.useState<Toggles>({
    airplane: false,
    wifi: true,
    bluetooth: true,
    focus: false,
  })

  const setToggle = (key: keyof Toggles, value: boolean) =>
    setToggles((current) => ({
      ...current,
      [key]: value,
      ...(key === "airplane" && value ? { wifi: false, bluetooth: false } : {}),
    }))

  const matches = (label: string) =>
    !query || label.toLowerCase().includes(query.toLowerCase())
  const visible = groups
    .map((rows) => rows.filter((row) => matches(row.label)))
    .filter((rows): rows is [Row, ...Row[]] => rows.length > 0)

  return (
    <section className="flex min-h-170 items-center justify-center bg-muted sm:p-5">
      <div className="relative flex w-full flex-col overflow-hidden bg-muted sm:h-165 sm:w-90 sm:rounded-[3.125rem] sm:border-[10px] sm:border-black sm:shadow-[0_0_0_1.5px_var(--color-muted-foreground),0_30px_60px_-20px_rgb(0_0_0/0.45)] dark:bg-background">
        <div
          aria-hidden
          className="hidden h-12.5 shrink-0 items-center justify-between ps-8.5 pe-7 pt-1 text-base font-semibold sm:flex"
        >
          <span>9:41</span>
          <span className="flex items-center gap-1.25">
            <CellSignalFullIcon className="size-3.75" />
            {toggles.wifi && (
              <WifiHighIcon className="size-3.75" />
            )}
            <span className="flex h-2.75 w-6 rounded-[3px] border border-current p-[1.5px]">
              <span className="flex-1 rounded-[1.5px] bg-current" />
            </span>
          </span>
        </div>
        <span
          aria-hidden
          className="absolute top-2.75 left-1/2 hidden h-7.25 w-25 -translate-x-1/2 rounded-full bg-black sm:block"
        />
        <div className="no-scrollbar flex flex-1 flex-col gap-4.5 overflow-y-auto px-4 pt-6 pb-6 sm:pt-1.5">
          <h1 className="px-1 text-4xl font-bold tracking-tight">Settings</h1>
          <InputGroup className="-mt-2 h-9 shrink-0 rounded-[0.625rem] dark:bg-muted">
            <InputGroupAddon>
              <MagnifyingGlassIcon />
            </InputGroupAddon>
            <InputGroupInput
              type="search"
              value={query}
              onChange={(event) => setQuery(event.target.value)}
              placeholder="Search"
              aria-label="Search settings"
              className="text-lg"
            />
          </InputGroup>
          {!query && (
            <ItemGroup variant="inset" className={groupClass}>
              <div role="listitem" className="contents">
                <Item
                  size="sm"
                  render={<RowLink href="#account" />}
                  className="gap-3.5 py-2.5"
                >
                  <Avatar className="size-14.5">
                    <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-xl text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                      JR
                    </AvatarFallback>
                  </Avatar>
                  <ItemContent className="gap-0.5">
                    <ItemTitle className="text-xl font-semibold">
                      Jamie Rivera
                    </ItemTitle>
                    <ItemDescription className="text-sm">
                      suiss Account, Cloud and more
                    </ItemDescription>
                  </ItemContent>
                  <CaretRightIcon
                    className="size-3.5 text-muted-foreground rtl:rotate-180"
                  />
                </Item>
              </div>
            </ItemGroup>
          )}
          {visible.map((rows) => (
            <ItemGroup
              key={rows[0].label}
              variant="inset"
              className={`${groupClass} **:data-[slot=item-separator]:ms-14.25`}
            >
              {rows.map((row, index) => {
                const detail = row.detail?.(toggles)
                const switchId = `${idPrefix}-${row.toggle}`
                return (
                  <div key={row.label} role="listitem" className="contents">
                    {index > 0 && <ItemSeparator />}
                    <Item
                      size="sm"
                      render={
                        row.toggle ? (
                          <div />
                        ) : (
                          <RowLink href={`#${row.label}`} />
                        )
                      }
                      className="min-h-11 gap-3.5 py-1.5"
                    >
                      <ItemMedia
                        className={`size-7.25 rounded-[7px] text-white ${row.tint}`}
                      >
                        {row.icon}
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle className="text-base font-normal">
                          {row.toggle ? (
                            <label htmlFor={switchId}>{row.label}</label>
                          ) : (
                            row.label
                          )}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions className="gap-1.5 text-base text-muted-foreground">
                        {row.toggle ? (
                          <Switch
                            id={switchId}
                            checked={toggles[row.toggle]}
                            onCheckedChange={(value) =>
                              setToggle(row.toggle as keyof Toggles, value)
                            }
                          />
                        ) : (
                          <>
                            {detail}
                            <CaretRightIcon
                              className="size-3.5 text-muted-foreground rtl:rotate-180"
                            />
                          </>
                        )}
                      </ItemActions>
                    </Item>
                  </div>
                )
              })}
            </ItemGroup>
          ))}
          {visible.length === 0 && (
            <p className="p-7.5 text-center text-base text-muted-foreground">
              No Results
            </p>
          )}
        </div>
      </div>
    </section>
  )
}
```

A phone-style Settings screen with a search field, an account row and grouped rows that either open a detail page or hold a switch. Search filters rows by label, and turning on Airplane Mode also turns off Wi-Fi and Bluetooth. From small tablets up the screen sits in a phone frame; on the smallest screens it fills the width. Use it for mobile app settings or any grouped preferences list.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/settings-device
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks settings-device
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Input Group](https://ui.suiss.app/docs/components/input-group), [Item](https://ui.suiss.app/docs/components/item), [Switch](https://ui.suiss.app/docs/components/switch).

3. Copy the source into `components/blocks/settings-device.tsx`:

```tsx title="components/blocks/settings-device.tsx"
"use client"

import {
  AirplaneIcon,
  BellIcon,
  BluetoothIcon,
  CaretRightIcon,
  CellSignalFullIcon,
  GearIcon,
  HourglassIcon,
  MagnifyingGlassIcon,
  MoonIcon,
  SpeakerHighIcon,
  SunIcon,
  WifiHighIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Switch } from "@/components/ui/switch"

type Toggles = {
  airplane: boolean
  wifi: boolean
  bluetooth: boolean
  focus: boolean
}

type Row = {
  label: string
  icon: React.ReactNode
  tint: string
  toggle?: keyof Toggles
  detail?: (toggles: Toggles) => string
}

const groups: Row[][] = [
  [
    {
      label: "Airplane Mode",
      icon: (
        <AirplaneIcon className="size-4.25" />
      ),
      tint: "bg-orange-500",
      toggle: "airplane",
    },
    {
      label: "Wi-Fi",
      icon: (
        <WifiHighIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.wifi ? "Home" : "Off"),
    },
    {
      label: "Bluetooth",
      icon: (
        <BluetoothIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.bluetooth ? "On" : "Off"),
    },
    {
      label: "Cellular",
      icon: (
        <CellSignalFullIcon className="size-4.25" />
      ),
      tint: "bg-green-500",
      detail: (toggles) => (toggles.airplane ? "Off" : ""),
    },
  ],
  [
    {
      label: "Notifications",
      icon: (
        <BellIcon className="size-4.25" />
      ),
      tint: "bg-red-500",
    },
    {
      label: "Sounds & Haptics",
      icon: (
        <SpeakerHighIcon className="size-4.25" />
      ),
      tint: "bg-pink-500",
    },
    {
      label: "Focus",
      icon: (
        <MoonIcon className="size-4.25" />
      ),
      tint: "bg-indigo-500",
      toggle: "focus",
    },
    {
      label: "Screen Time",
      icon: (
        <HourglassIcon className="size-4.25" />
      ),
      tint: "bg-indigo-500",
    },
  ],
  [
    {
      label: "General",
      icon: (
        <GearIcon className="size-4.25" />
      ),
      tint: "bg-zinc-500",
    },
    {
      label: "Display & Brightness",
      icon: (
        <SunIcon className="size-4.25" />
      ),
      tint: "bg-blue-500",
    },
  ],
]

function RowLink(props: React.ComponentProps<"a">) {
  return <a {...props} />
}

const groupClass = "shrink-0 bg-background dark:bg-muted"

export function SettingsDevice() {
  const idPrefix = React.useId()
  const [query, setQuery] = React.useState("")
  const [toggles, setToggles] = React.useState<Toggles>({
    airplane: false,
    wifi: true,
    bluetooth: true,
    focus: false,
  })

  const setToggle = (key: keyof Toggles, value: boolean) =>
    setToggles((current) => ({
      ...current,
      [key]: value,
      ...(key === "airplane" && value ? { wifi: false, bluetooth: false } : {}),
    }))

  const matches = (label: string) =>
    !query || label.toLowerCase().includes(query.toLowerCase())
  const visible = groups
    .map((rows) => rows.filter((row) => matches(row.label)))
    .filter((rows): rows is [Row, ...Row[]] => rows.length > 0)

  return (
    <section className="flex min-h-170 items-center justify-center bg-muted sm:p-5">
      <div className="relative flex w-full flex-col overflow-hidden bg-muted sm:h-165 sm:w-90 sm:rounded-[3.125rem] sm:border-[10px] sm:border-black sm:shadow-[0_0_0_1.5px_var(--color-muted-foreground),0_30px_60px_-20px_rgb(0_0_0/0.45)] dark:bg-background">
        <div
          aria-hidden
          className="hidden h-12.5 shrink-0 items-center justify-between ps-8.5 pe-7 pt-1 text-base font-semibold sm:flex"
        >
          <span>9:41</span>
          <span className="flex items-center gap-1.25">
            <CellSignalFullIcon className="size-3.75" />
            {toggles.wifi && (
              <WifiHighIcon className="size-3.75" />
            )}
            <span className="flex h-2.75 w-6 rounded-[3px] border border-current p-[1.5px]">
              <span className="flex-1 rounded-[1.5px] bg-current" />
            </span>
          </span>
        </div>
        <span
          aria-hidden
          className="absolute top-2.75 left-1/2 hidden h-7.25 w-25 -translate-x-1/2 rounded-full bg-black sm:block"
        />
        <div className="no-scrollbar flex flex-1 flex-col gap-4.5 overflow-y-auto px-4 pt-6 pb-6 sm:pt-1.5">
          <h1 className="px-1 text-4xl font-bold tracking-tight">Settings</h1>
          <InputGroup className="-mt-2 h-9 shrink-0 rounded-[0.625rem] dark:bg-muted">
            <InputGroupAddon>
              <MagnifyingGlassIcon />
            </InputGroupAddon>
            <InputGroupInput
              type="search"
              value={query}
              onChange={(event) => setQuery(event.target.value)}
              placeholder="Search"
              aria-label="Search settings"
              className="text-lg"
            />
          </InputGroup>
          {!query && (
            <ItemGroup variant="inset" className={groupClass}>
              <div role="listitem" className="contents">
                <Item
                  size="sm"
                  render={<RowLink href="#account" />}
                  className="gap-3.5 py-2.5"
                >
                  <Avatar className="size-14.5">
                    <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-xl text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                      JR
                    </AvatarFallback>
                  </Avatar>
                  <ItemContent className="gap-0.5">
                    <ItemTitle className="text-xl font-semibold">
                      Jamie Rivera
                    </ItemTitle>
                    <ItemDescription className="text-sm">
                      suiss Account, Cloud and more
                    </ItemDescription>
                  </ItemContent>
                  <CaretRightIcon
                    className="size-3.5 text-muted-foreground rtl:rotate-180"
                  />
                </Item>
              </div>
            </ItemGroup>
          )}
          {visible.map((rows) => (
            <ItemGroup
              key={rows[0].label}
              variant="inset"
              className={`${groupClass} **:data-[slot=item-separator]:ms-14.25`}
            >
              {rows.map((row, index) => {
                const detail = row.detail?.(toggles)
                const switchId = `${idPrefix}-${row.toggle}`
                return (
                  <div key={row.label} role="listitem" className="contents">
                    {index > 0 && <ItemSeparator />}
                    <Item
                      size="sm"
                      render={
                        row.toggle ? (
                          <div />
                        ) : (
                          <RowLink href={`#${row.label}`} />
                        )
                      }
                      className="min-h-11 gap-3.5 py-1.5"
                    >
                      <ItemMedia
                        className={`size-7.25 rounded-[7px] text-white ${row.tint}`}
                      >
                        {row.icon}
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle className="text-base font-normal">
                          {row.toggle ? (
                            <label htmlFor={switchId}>{row.label}</label>
                          ) : (
                            row.label
                          )}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions className="gap-1.5 text-base text-muted-foreground">
                        {row.toggle ? (
                          <Switch
                            id={switchId}
                            checked={toggles[row.toggle]}
                            onCheckedChange={(value) =>
                              setToggle(row.toggle as keyof Toggles, value)
                            }
                          />
                        ) : (
                          <>
                            {detail}
                            <CaretRightIcon
                              className="size-3.5 text-muted-foreground rtl:rotate-180"
                            />
                          </>
                        )}
                      </ItemActions>
                    </Item>
                  </div>
                )
              })}
            </ItemGroup>
          ))}
          {visible.length === 0 && (
            <p className="p-7.5 text-center text-base text-muted-foreground">
              No Results
            </p>
          )}
        </div>
      </div>
    </section>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { SettingsDevice } from "@/components/blocks/settings-device"
```

```tsx
<SettingsDevice />
```

Edit `groups`; each row has a label, icon, tint and either a `toggle` key or a `detail` function that derives its status text from the toggles. Persist the toggles to your settings store, and remove the Airplane Mode coupling if it doesn't apply. Row links point to hash anchors, so swap in your routes.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-settings-device--default.light.webp)

```tsx title="uniwind/components/blocks/settings-device.tsx"
import { cn } from "cn"
import {
  AirplaneIcon,
  BellIcon,
  BluetoothIcon,
  CaretRightIcon,
  CellSignalFullIcon,
  GearIcon,
  HourglassIcon,
  MagnifyingGlassIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  SpeakerHighIcon,
  SunIcon,
  WifiHighIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, View } from "react-native"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  Header,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

type Toggles = {
  airplane: boolean
  wifi: boolean
  bluetooth: boolean
  focus: boolean
}

type Row = {
  label: string
  icon: PhosphorIcon
  tint: string
  toggle?: keyof Toggles
  detail?: (toggles: Toggles) => string
}

const GROUPS: Row[][] = [
  [
    {
      label: "Airplane Mode",
      icon: AirplaneIcon,
      tint: "bg-orange-500",
      toggle: "airplane",
    },
    {
      label: "Wi-Fi",
      icon: WifiHighIcon,
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.wifi ? "Home" : "Off"),
    },
    {
      label: "Bluetooth",
      icon: BluetoothIcon,
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.bluetooth ? "On" : "Off"),
    },
    {
      label: "Cellular",
      icon: CellSignalFullIcon,
      tint: "bg-green-500",
      detail: (toggles) => (toggles.airplane ? "Off" : ""),
    },
  ],
  [
    { label: "Notifications", icon: BellIcon, tint: "bg-red-500" },
    { label: "Sounds & Haptics", icon: SpeakerHighIcon, tint: "bg-pink-500" },
    { label: "Focus", icon: MoonIcon, tint: "bg-indigo-500", toggle: "focus" },
    { label: "Screen Time", icon: HourglassIcon, tint: "bg-indigo-500" },
  ],
  [
    { label: "General", icon: GearIcon, tint: "bg-zinc-500" },
    { label: "Display & Brightness", icon: SunIcon, tint: "bg-blue-500" },
  ],
]

const GROUP_CLASS = "bg-background dark:bg-muted"

function Chevron() {
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      className={cn(
        "size-3.5 text-muted-foreground",
        I18nManager.isRTL && "rotate-180"
      )}
    />
  )
}

function SettingsDevice({
  onAccountPress,
  onRowPress,
}: {
  onAccountPress?: () => void
  onRowPress?: (label: string) => void
}) {
  const scroll = useHeaderScroll()
  const [query, setQuery] = React.useState("")
  const [toggles, setToggles] = React.useState<Toggles>({
    airplane: false,
    wifi: true,
    bluetooth: true,
    focus: false,
  })

  const setToggle = (key: keyof Toggles, value: boolean) =>
    setToggles((current) => ({
      ...current,
      [key]: value,
      ...(key === "airplane" && value ? { wifi: false, bluetooth: false } : {}),
    }))

  const matches = (label: string) =>
    !query || label.toLowerCase().includes(query.toLowerCase())
  const visible = GROUPS.map((rows) =>
    rows.filter((row) => matches(row.label))
  ).filter((rows): rows is [Row, ...Row[]] => rows.length > 0)

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      className="dark:bg-background"
    >
      <Header scroll={scroll}>
        <HeaderTitle>Settings</HeaderTitle>
      </Header>
      <HeaderScrollView
        scroll={scroll}
        keyboardShouldPersistTaps="handled"
        keyboardDismissMode="on-drag"
      >
        <HeaderLargeTitle>Settings</HeaderLargeTitle>
        <View className="gap-4.5 px-4 pb-8">
          <InputGroup className="h-9 rounded-[0.625rem] dark:bg-muted">
            <InputGroupAddon>
              <Icon
                as={MagnifyingGlassIcon}
                weight="bold"
                className="size-4 text-muted-foreground"
              />
            </InputGroupAddon>
            <InputGroupInput
              value={query}
              onChangeText={setQuery}
              placeholder="Search"
              accessibilityLabel="Search settings"
              returnKeyType="search"
              autoCorrect={false}
              clearButtonMode="while-editing"
              className="text-[18px]"
            />
          </InputGroup>
          {query ? null : (
            <ItemGroup variant="inset" className={GROUP_CLASS}>
              <Item
                size="sm"
                onPress={onAccountPress}
                className="gap-3.5 py-2.5"
              >
                <Avatar size="xl" className="size-14.5">
                  <AvatarFallback className="bg-indigo-500">
                    <Text className="text-xl">JR</Text>
                  </AvatarFallback>
                </Avatar>
                <ItemContent className="gap-0.5">
                  <ItemTitle className="text-xl font-semibold">
                    Jamie Rivera
                  </ItemTitle>
                  <ItemDescription className="text-sm">
                    suiss Account, Cloud and more
                  </ItemDescription>
                </ItemContent>
                <Chevron />
              </Item>
            </ItemGroup>
          )}
          {visible.map((rows) => (
            <ItemGroup
              key={rows[0].label}
              variant="inset"
              className={GROUP_CLASS}
            >
              {rows.map((row, index) => {
                const detail = row.detail?.(toggles)
                const toggle = row.toggle
                return (
                  <React.Fragment key={row.label}>
                    {index > 0 ? <ItemSeparator className="ms-14.25" /> : null}
                    <Item
                      size="sm"
                      onPress={
                        toggle ? undefined : () => onRowPress?.(row.label)
                      }
                      className="min-h-11 gap-3.5 py-1.5"
                    >
                      <ItemMedia
                        className={cn("size-7.25 rounded-[7px]", row.tint)}
                      >
                        <Icon
                          as={row.icon}
                          weight="bold"
                          className="size-4.25 text-primary-foreground"
                        />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle
                          nativeID={
                            toggle ? `settings-device-${toggle}` : undefined
                          }
                          className="font-normal"
                        >
                          {row.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions className="gap-1.5">
                        {toggle ? (
                          <Switch
                            checked={toggles[toggle]}
                            onCheckedChange={(value) =>
                              setToggle(toggle, value)
                            }
                            accessibilityLabelledBy={`settings-device-${toggle}`}
                          />
                        ) : (
                          <>
                            {detail ? <Text>{detail}</Text> : null}
                            <Chevron />
                          </>
                        )}
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                )
              })}
            </ItemGroup>
          ))}
          {visible.length === 0 ? (
            <Text className="p-7.5 text-center text-base text-muted-foreground">
              No Results
            </Text>
          ) : null}
        </View>
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsDevice }
```

```swift title="swift/Blocks/SettingsDevice.swift"
import PhosphorSwift
import SwiftUI

public struct SettingsDevice: View {
  struct Toggles: Equatable, Sendable {
    var airplane = false
    var wifi = true
    var bluetooth = true
    var focus = false
  }

  struct Row {
    let label: String
    let icon: Image
    let tint: Color
    var toggle: WritableKeyPath<Toggles, Bool>?
    var detail: ((Toggles) -> LocalizedStringKey?)?
  }

  static var groups: [[Row]] {
    [
      [
        Row(label: "Airplane Mode", icon: Ph.airplane.bold, tint: Palette.orange500, toggle: \.airplane),
        Row(label: "Wi-Fi", icon: Ph.wifiHigh.bold, tint: Palette.blue500, detail: { $0.wifi ? "Home" : "Off" }),
        Row(label: "Bluetooth", icon: Ph.bluetooth.bold, tint: Palette.blue500, detail: { $0.bluetooth ? "On" : "Off" }),
        Row(label: "Cellular", icon: Ph.cellSignalFull.bold, tint: Palette.green500, detail: { $0.airplane ? "Off" : nil }),
      ],
      [
        Row(label: "Notifications", icon: Ph.bell.bold, tint: Palette.red500),
        Row(label: "Sounds & Haptics", icon: Ph.speakerHigh.bold, tint: Palette.pink500),
        Row(label: "Focus", icon: Ph.moon.bold, tint: Palette.indigo500, toggle: \.focus),
        Row(label: "Screen Time", icon: Ph.hourglass.bold, tint: Palette.indigo500),
      ],
      [
        Row(label: "General", icon: Ph.gear.bold, tint: Palette.zinc500),
        Row(label: "Display & Brightness", icon: Ph.sun.bold, tint: Palette.blue500),
      ],
    ]
  }

  private var onAccountPress: (() -> Void)?
  private var onRowPress: ((String) -> Void)?
  @State private var query: String
  @State private var toggles = Toggles()

  public init(onAccountPress: (() -> Void)? = nil, onRowPress: ((String) -> Void)? = nil) {
    self.init(query: "")
    self.onAccountPress = onAccountPress
    self.onRowPress = onRowPress
  }

  init(query: String) {
    _query = State(initialValue: query)
  }

  private var visible: [[Row]] {
    Self.groups
      .map { rows in rows.filter { query.isEmpty || $0.label.localizedCaseInsensitiveContains(query) } }
      .filter { !$0.isEmpty }
  }

  public var body: some View {
    Screen {
      HeaderScrollView {
        HeaderLargeTitle("Settings")
        VStack(spacing: 18) {
          InputGroup {
            InputGroupAddon {
              Ph.magnifyingGlass.bold.frame(width: 16, height: 16)
            }
            InputGroupInput("Search", text: $query)
              .submitLabel(.search)
              .accessibilityLabel(Text("Search settings"))
          }
          if query.isEmpty {
            ItemGroup(.inset) {
              Item(size: .sm, action: { onAccountPress?() }) {
                ItemMedia {
                  Avatar(size: .xl) {
                    Text(verbatim: "JR")
                      .scaledFont(.xl, weight: .semibold)
                      .foregroundStyle(EsuissColor.primaryForeground)
                      .frame(maxWidth: .infinity, maxHeight: .infinity)
                      .background(
                        Palette.indigo500,
                        in: Circle()
                      )
                  }
                  .frame(width: 58, height: 58)
                }
                ItemContent {
                  ItemTitle {
                    Text(verbatim: "Jamie Rivera")
                      .scaledFont(.xl, weight: .semibold)
                  }
                  ItemDescription("suiss Account, Cloud and more")
                }
                ItemActions {
                  SettingsDeviceChevron()
                }
              }
            }
          }
          ForEach(Array(visible.enumerated()), id: \.offset) { _, rows in
            ItemGroup(.inset) {
              ForEach(Array(rows.enumerated()), id: \.element.label) { index, row in
                if index > 0 {
                  Separator().padding(.leading, 57)
                }
                SettingsDeviceRow(row: row, toggles: $toggles, onPress: onRowPress)
              }
            }
          }
          if visible.isEmpty {
            Text("No Results")
              .scaledFont(.base)
              .foregroundStyle(EsuissColor.mutedForeground)
              .frame(maxWidth: .infinity)
              .padding(30)
          }
        }
        .padding(.horizontal, 16)
        .padding(.bottom, 24)
      } header: {
        Header {
          HeaderTitle("Settings")
        }
      }
    }
  }
}

private struct SettingsDeviceRow: View {
  let row: SettingsDevice.Row
  @Binding var toggles: SettingsDevice.Toggles
  let onPress: ((String) -> Void)?

  var body: some View {
    if let key = row.toggle {
      Item(size: .sm) {
        media
        ItemContent {
          ItemTitle {
            Text(LocalizedStringKey(row.label)).fontWeight(.regular)
          }
        }
        ItemActions {
          Toggle(isOn: binding(key)) { Text(LocalizedStringKey(row.label)) }
            .labelsHidden()
            .accessibilityLabel(Text(LocalizedStringKey(row.label)))
          .toggleStyle(SwitchStyle())
        }
      }
    } else {
      Item(size: .sm, action: { onPress?(row.label) }) {
        media
        ItemContent {
          ItemTitle {
            Text(LocalizedStringKey(row.label)).fontWeight(.regular)
          }
        }
        ItemActions {
          if let detail = row.detail?(toggles) {
            Text(detail)
              .scaledFont(.base)
              .foregroundStyle(EsuissColor.mutedForeground)
          }
          SettingsDeviceChevron()
        }
      }
    }
  }

  private var media: some View {
    ItemMedia {
      row.icon
        .frame(width: 17, height: 17)
        .foregroundStyle(EsuissColor.primaryForeground)
        .frame(width: 29, height: 29)
        .background(row.tint, in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
    }
  }

  private func binding(_ key: WritableKeyPath<SettingsDevice.Toggles, Bool>) -> Binding<Bool> {
    Binding(
      get: { toggles[keyPath: key] },
      set: { value in
        toggles[keyPath: key] = value
        if key == \SettingsDevice.Toggles.airplane && value {
          toggles.wifi = false
          toggles.bluetooth = false
        }
      }
    )
  }
}

private struct SettingsDeviceChevron: View {
  var body: some View {
    Ph.caretRight.bold
      .frame(width: 14, height: 14)
      .foregroundStyle(EsuissColor.mutedForeground)
      .flipsForRightToLeftLayoutDirection(true)
      .accessibilityHidden(true)
  }
}
```

```kt title="kotlin/blocks/SettingsDevice.kt"
package com.esuiss.ui.blocks

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.bold.Airplane
import com.adamglin.phosphoricons.bold.Bell
import com.adamglin.phosphoricons.bold.Bluetooth
import com.adamglin.phosphoricons.bold.CaretRight
import com.adamglin.phosphoricons.bold.CellSignalFull
import com.adamglin.phosphoricons.bold.Gear
import com.adamglin.phosphoricons.bold.Hourglass
import com.adamglin.phosphoricons.bold.MagnifyingGlass
import com.adamglin.phosphoricons.bold.Moon
import com.adamglin.phosphoricons.bold.SpeakerHigh
import com.adamglin.phosphoricons.bold.Sun
import com.adamglin.phosphoricons.bold.WifiHigh
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Header
import com.esuiss.ui.components.HeaderLargeTitle
import com.esuiss.ui.components.HeaderTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.InputGroup
import com.esuiss.ui.components.InputGroupAddon
import com.esuiss.ui.components.InputGroupInput
import com.esuiss.ui.components.Item
import com.esuiss.ui.components.ItemActions
import com.esuiss.ui.components.ItemContent
import com.esuiss.ui.components.ItemDescription
import com.esuiss.ui.components.ItemGroup
import com.esuiss.ui.components.ItemGroupVariant
import com.esuiss.ui.components.ItemMedia
import com.esuiss.ui.components.ItemSeparator
import com.esuiss.ui.components.ItemSize
import com.esuiss.ui.components.ItemTitle
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Switch
import com.esuiss.ui.components.rememberHeaderState
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography

@Immutable
private data class DeviceToggles(
    val airplane: Boolean = false,
    val wifi: Boolean = true,
    val bluetooth: Boolean = true,
    val focus: Boolean = false,
)

private enum class ToggleKey { Airplane, Focus }

private class DeviceRow(
    val label: String,
    val icon: ImageVector,
    val tint: (Colors) -> Color,
    val toggle: ToggleKey? = null,
    val detail: ((DeviceToggles) -> String)? = null,
)

private val DeviceGroups = listOf(
    listOf(
        DeviceRow("Airplane Mode", PhosphorIcons.Bold.Airplane, { Palette.orange500 }, toggle = ToggleKey.Airplane),
        DeviceRow("Wi-Fi", PhosphorIcons.Bold.WifiHigh, { Palette.blue500 }, detail = { if (it.wifi) "Home" else "Off" }),
        DeviceRow("Bluetooth", PhosphorIcons.Bold.Bluetooth, { Palette.blue500 }, detail = { if (it.bluetooth) "On" else "Off" }),
        DeviceRow("Cellular", PhosphorIcons.Bold.CellSignalFull, { Palette.green500 }, detail = { if (it.airplane) "Off" else "" }),
    ),
    listOf(
        DeviceRow("Notifications", PhosphorIcons.Bold.Bell, { Palette.red500 }),
        DeviceRow("Sounds & Haptics", PhosphorIcons.Bold.SpeakerHigh, { Palette.pink500 }),
        DeviceRow("Focus", PhosphorIcons.Bold.Moon, { Palette.indigo500 }, toggle = ToggleKey.Focus),
        DeviceRow("Screen Time", PhosphorIcons.Bold.Hourglass, { Palette.indigo500 }),
    ),
    listOf(
        DeviceRow("General", PhosphorIcons.Bold.Gear, { Palette.zinc500 }),
        DeviceRow("Display & Brightness", PhosphorIcons.Bold.Sun, { Palette.blue500 }),
    ),
)

@Composable
fun SettingsDevice(
    modifier: Modifier = Modifier,
    onAccountPress: () -> Unit = {},
    onRowPress: (label: String) -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var query by rememberSaveable { mutableStateOf("") }
    var toggles by remember { mutableStateOf(DeviceToggles()) }
    val header = rememberHeaderState()

    fun setToggle(key: ToggleKey, value: Boolean) {
        toggles = when (key) {
            ToggleKey.Airplane -> if (value) toggles.copy(airplane = true, wifi = false, bluetooth = false) else toggles.copy(airplane = false)
            ToggleKey.Focus -> toggles.copy(focus = value)
        }
    }

    fun isOn(key: ToggleKey) = when (key) {
        ToggleKey.Airplane -> toggles.airplane
        ToggleKey.Focus -> toggles.focus
    }

    val visible = DeviceGroups
        .map { rows -> rows.filter { query.isBlank() || it.label.contains(query.trim(), ignoreCase = true) } }
        .filter { it.isNotEmpty() }

    Screen(modifier) {
        Column(
            Modifier
                .fillMaxSize()
                .nestedScroll(header.nestedScrollConnection),
        ) {
            Header(state = header) {
                HeaderTitle("Settings")
                HeaderLargeTitle("Settings")
            }
            Column(
                modifier = Modifier
                    .weight(1f)
                    .verticalScroll(rememberScrollState())
                    .padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 24.dp),
                verticalArrangement = Arrangement.spacedBy(18.dp),
            ) {
                InputGroup {
                    InputGroupAddon {
                        Icon(PhosphorIcons.Bold.MagnifyingGlass, contentDescription = null)
                    }
                    InputGroupInput(value = query, onValueChange = { query = it }, placeholder = "Search")
                }
                if (query.isBlank()) {
                    ItemGroup(variant = ItemGroupVariant.Inset) {
                        Item(size = ItemSize.Sm, onClick = onAccountPress) {
                            Avatar(
                                modifier = Modifier.size(58.dp),
                                size = AvatarSize.Xl,
                                fallback = {
                                    Box(
                                        Modifier
                                            .fillMaxSize()
                                            .background(Palette.indigo500, CircleShape),
                                        contentAlignment = Alignment.Center,
                                    ) {
                                        BasicText("JR", style = Typography.xl.copy(color = colors.primaryForeground, fontWeight = FontWeight.SemiBold))
                                    }
                                },
                            )
                            ItemContent {
                                BasicText("Jamie Rivera", style = Typography.xl.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                                ItemDescription("suiss Account, Cloud and more")
                            }
                            ItemActions {
                                Icon(PhosphorIcons.Bold.CaretRight, contentDescription = null, size = 14.dp, tint = colors.mutedForeground)
                            }
                        }
                    }
                }
                visible.forEach { rows ->
                    ItemGroup(variant = ItemGroupVariant.Inset) {
                        rows.forEachIndexed { index, row ->
                            if (index > 0) ItemSeparator(Modifier.padding(start = 41.dp))
                            val toggle = row.toggle
                            Item(
                                size = ItemSize.Sm,
                                onClick = if (toggle == null) ({ onRowPress(row.label) }) else null,
                            ) {
                                ItemMedia(
                                    Modifier
                                        .size(29.dp)
                                        .background(row.tint(colors), RoundedCornerShape(7.dp)),
                                ) {
                                    Icon(row.icon, contentDescription = null, size = 17.dp, tint = colors.primaryForeground)
                                }
                                ItemContent {
                                    ItemTitle {
                                        BasicText(row.label, style = Typography.base.copy(color = colors.foreground), maxLines = 1)
                                    }
                                }
                                ItemActions {
                                    if (toggle != null) {
                                        Switch(checked = isOn(toggle), onCheckedChange = { setToggle(toggle, it) })
                                    } else {
                                        val detail = row.detail?.invoke(toggles).orEmpty()
                                        if (detail.isNotEmpty()) {
                                            BasicText(detail, style = Typography.base.copy(color = colors.mutedForeground))
                                        }
                                        Icon(PhosphorIcons.Bold.CaretRight, contentDescription = null, size = 14.dp, tint = colors.mutedForeground)
                                    }
                                }
                            }
                        }
                    }
                }
                if (visible.isEmpty()) {
                    BasicText(
                        "No Results",
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(30.dp),
                        style = Typography.base.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                    )
                }
            }
        }
    }
}
```

A large-title `Header` sits over an `InputGroup` search field, an account `Item` with an `Avatar`, and inset `ItemGroup` lists of rows with tinted icons. Airplane Mode and Focus use a `Switch`; Wi-Fi, Bluetooth and the other rows show a detail value and a chevron. Typing filters the rows and shows a message when nothing matches.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/settings-device
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add settings-device
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add settings-device --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add settings-device --platform kotlin
```

The CLI also adds what it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input Group](https://ui.suiss.app/docs/components/input-group), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input Group](https://ui.suiss.app/docs/components/input-group), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/settings-device.tsx`:

- `uniwind/components/blocks/settings-device.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input Group](https://ui.suiss.app/docs/components/input-group), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/settings-device.tsx`:

```tsx title="nativewind/components/blocks/settings-device.tsx"
import { cn } from "cn"
import {
  AirplaneIcon,
  BellIcon,
  BluetoothIcon,
  CaretRightIcon,
  CellSignalFullIcon,
  GearIcon,
  HourglassIcon,
  MagnifyingGlassIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  SpeakerHighIcon,
  SunIcon,
  WifiHighIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, View } from "react-native"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  Header,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

type Toggles = {
  airplane: boolean
  wifi: boolean
  bluetooth: boolean
  focus: boolean
}

type Row = {
  label: string
  icon: PhosphorIcon
  tint: string
  toggle?: keyof Toggles
  detail?: (toggles: Toggles) => string
}

const GROUPS: Row[][] = [
  [
    {
      label: "Airplane Mode",
      icon: AirplaneIcon,
      tint: "bg-orange-500",
      toggle: "airplane",
    },
    {
      label: "Wi-Fi",
      icon: WifiHighIcon,
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.wifi ? "Home" : "Off"),
    },
    {
      label: "Bluetooth",
      icon: BluetoothIcon,
      tint: "bg-blue-500",
      detail: (toggles) => (toggles.bluetooth ? "On" : "Off"),
    },
    {
      label: "Cellular",
      icon: CellSignalFullIcon,
      tint: "bg-green-500",
      detail: (toggles) => (toggles.airplane ? "Off" : ""),
    },
  ],
  [
    { label: "Notifications", icon: BellIcon, tint: "bg-red-500" },
    { label: "Sounds & Haptics", icon: SpeakerHighIcon, tint: "bg-pink-500" },
    { label: "Focus", icon: MoonIcon, tint: "bg-indigo-500", toggle: "focus" },
    { label: "Screen Time", icon: HourglassIcon, tint: "bg-indigo-500" },
  ],
  [
    { label: "General", icon: GearIcon, tint: "bg-zinc-500" },
    { label: "Display & Brightness", icon: SunIcon, tint: "bg-blue-500" },
  ],
]

const GROUP_CLASS = "bg-background dark:bg-muted"

function Chevron() {
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      className={cn(
        "size-3.5 text-muted-foreground",
        I18nManager.isRTL && "rotate-180"
      )}
    />
  )
}

function SettingsDevice({
  onAccountPress,
  onRowPress,
}: {
  onAccountPress?: () => void
  onRowPress?: (label: string) => void
}) {
  const scroll = useHeaderScroll()
  const [query, setQuery] = React.useState("")
  const [toggles, setToggles] = React.useState<Toggles>({
    airplane: false,
    wifi: true,
    bluetooth: true,
    focus: false,
  })

  const setToggle = (key: keyof Toggles, value: boolean) =>
    setToggles((current) => ({
      ...current,
      [key]: value,
      ...(key === "airplane" && value ? { wifi: false, bluetooth: false } : {}),
    }))

  const matches = (label: string) =>
    !query || label.toLowerCase().includes(query.toLowerCase())
  const visible = GROUPS.map((rows) =>
    rows.filter((row) => matches(row.label))
  ).filter((rows): rows is [Row, ...Row[]] => rows.length > 0)

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      className="dark:bg-background"
    >
      <Header scroll={scroll}>
        <HeaderTitle>Settings</HeaderTitle>
      </Header>
      <HeaderScrollView
        scroll={scroll}
        keyboardShouldPersistTaps="handled"
        keyboardDismissMode="on-drag"
      >
        <HeaderLargeTitle>Settings</HeaderLargeTitle>
        <View className="gap-[1.125rem] px-4 pb-8">
          <InputGroup className="h-9 rounded-[0.625rem] dark:bg-muted">
            <InputGroupAddon>
              <Icon
                as={MagnifyingGlassIcon}
                weight="bold"
                className="size-4 text-muted-foreground"
              />
            </InputGroupAddon>
            <InputGroupInput
              value={query}
              onChangeText={setQuery}
              placeholder="Search"
              accessibilityLabel="Search settings"
              returnKeyType="search"
              autoCorrect={false}
              clearButtonMode="while-editing"
              className="text-[18px]"
            />
          </InputGroup>
          {query ? null : (
            <ItemGroup variant="inset" className={GROUP_CLASS}>
              <Item
                size="sm"
                onPress={onAccountPress}
                className="gap-3.5 py-2.5"
              >
                <Avatar size="xl" className="size-[3.625rem]">
                  <AvatarFallback className="bg-indigo-500">
                    <Text className="text-xl">JR</Text>
                  </AvatarFallback>
                </Avatar>
                <ItemContent className="gap-0.5">
                  <ItemTitle className="text-xl font-semibold">
                    Jamie Rivera
                  </ItemTitle>
                  <ItemDescription className="text-sm">
                    suiss Account, Cloud and more
                  </ItemDescription>
                </ItemContent>
                <Chevron />
              </Item>
            </ItemGroup>
          )}
          {visible.map((rows) => (
            <ItemGroup
              key={rows[0].label}
              variant="inset"
              className={GROUP_CLASS}
            >
              {rows.map((row, index) => {
                const detail = row.detail?.(toggles)
                const toggle = row.toggle
                return (
                  <React.Fragment key={row.label}>
                    {index > 0 ? (
                      <ItemSeparator className="ms-[3.5625rem]" />
                    ) : null}
                    <Item
                      size="sm"
                      onPress={
                        toggle ? undefined : () => onRowPress?.(row.label)
                      }
                      className="min-h-11 gap-3.5 py-1.5"
                    >
                      <ItemMedia
                        className={cn(
                          "size-[1.8125rem] rounded-[7px]",
                          row.tint
                        )}
                      >
                        <Icon
                          as={row.icon}
                          weight="bold"
                          className="size-[1.0625rem] text-primary-foreground"
                        />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle
                          nativeID={
                            toggle ? `settings-device-${toggle}` : undefined
                          }
                          className="font-normal"
                        >
                          {row.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions className="gap-1.5">
                        {toggle ? (
                          <Switch
                            checked={toggles[toggle]}
                            onCheckedChange={(value) =>
                              setToggle(toggle, value)
                            }
                            accessibilityLabelledBy={`settings-device-${toggle}`}
                          />
                        ) : (
                          <>
                            {detail ? <Text>{detail}</Text> : null}
                            <Chevron />
                          </>
                        )}
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                )
              })}
            </ItemGroup>
          ))}
          {visible.length === 0 ? (
            <Text className="p-[1.875rem] text-center text-base text-muted-foreground">
              No Results
            </Text>
          ) : null}
        </View>
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsDevice }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input Group](https://ui.suiss.app/docs/components/input-group), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/settings-device.tsx`:

```tsx title="stylesheet/components/blocks/settings-device.tsx"
import {
  AirplaneIcon,
  BellIcon,
  BluetoothIcon,
  CaretRightIcon,
  CellSignalFullIcon,
  GearIcon,
  HourglassIcon,
  MagnifyingGlassIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  SpeakerHighIcon,
  SunIcon,
  WifiHighIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, StyleSheet, View } from "react-native"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  Header,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

type Tone = keyof ThemeColors | keyof typeof palette

function tone(colors: ThemeColors, key: Tone) {
  return { ...colors, ...palette }[key]
}

type Toggles = {
  airplane: boolean
  wifi: boolean
  bluetooth: boolean
  focus: boolean
}

type Row = {
  label: string
  icon: PhosphorIcon
  tint: Tone
  toggle?: keyof Toggles
  detail?: (toggles: Toggles) => string
}

const GROUPS: Row[][] = [
  [
    {
      label: "Airplane Mode",
      icon: AirplaneIcon,
      tint: "orange500",
      toggle: "airplane",
    },
    {
      label: "Wi-Fi",
      icon: WifiHighIcon,
      tint: "blue500",
      detail: (toggles) => (toggles.wifi ? "Home" : "Off"),
    },
    {
      label: "Bluetooth",
      icon: BluetoothIcon,
      tint: "blue500",
      detail: (toggles) => (toggles.bluetooth ? "On" : "Off"),
    },
    {
      label: "Cellular",
      icon: CellSignalFullIcon,
      tint: "green500",
      detail: (toggles) => (toggles.airplane ? "Off" : ""),
    },
  ],
  [
    { label: "Notifications", icon: BellIcon, tint: "red500" },
    { label: "Sounds & Haptics", icon: SpeakerHighIcon, tint: "pink500" },
    { label: "Focus", icon: MoonIcon, tint: "indigo500", toggle: "focus" },
    { label: "Screen Time", icon: HourglassIcon, tint: "indigo500" },
  ],
  [
    { label: "General", icon: GearIcon, tint: "zinc500" },
    { label: "Display & Brightness", icon: SunIcon, tint: "blue500" },
  ],
]

const styles = StyleSheet.create({
  body: { gap: 18, paddingHorizontal: 16, paddingBottom: 32 },
  search: { height: 36, borderRadius: 10 },
  account: { gap: 14, paddingVertical: 10 },
  row: { minHeight: 44, gap: 14, paddingVertical: 6 },
  tile: { width: 29, height: 29, borderRadius: 7 },
  empty: { ...fontSize.base, padding: 30, textAlign: "center" },
})

function Chevron() {
  const { colors } = useTheme()
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      size={14}
      color={colors.mutedForeground}
      style={
        I18nManager.isRTL ? { transform: [{ rotate: "180deg" }] } : undefined
      }
    />
  )
}

function SettingsDevice({
  onAccountPress,
  onRowPress,
}: {
  onAccountPress?: () => void
  onRowPress?: (label: string) => void
}) {
  const { colors, isDark } = useTheme()
  const scroll = useHeaderScroll()
  const [query, setQuery] = React.useState("")
  const [toggles, setToggles] = React.useState<Toggles>({
    airplane: false,
    wifi: true,
    bluetooth: true,
    focus: false,
  })

  const setToggle = (key: keyof Toggles, value: boolean) =>
    setToggles((current) => ({
      ...current,
      [key]: value,
      ...(key === "airplane" && value ? { wifi: false, bluetooth: false } : {}),
    }))

  const matches = (label: string) =>
    !query || label.toLowerCase().includes(query.toLowerCase())
  const visible = GROUPS.map((rows) =>
    rows.filter((row) => matches(row.label))
  ).filter((rows): rows is [Row, ...Row[]] => rows.length > 0)

  const group = {
    backgroundColor: isDark ? colors.muted : colors.background,
  }

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      style={isDark ? { backgroundColor: colors.background } : undefined}
    >
      <Header scroll={scroll}>
        <HeaderTitle>Settings</HeaderTitle>
      </Header>
      <HeaderScrollView
        scroll={scroll}
        keyboardShouldPersistTaps="handled"
        keyboardDismissMode="on-drag"
      >
        <HeaderLargeTitle>Settings</HeaderLargeTitle>
        <View style={styles.body}>
          <InputGroup
            style={[styles.search, isDark && { backgroundColor: colors.muted }]}
          >
            <InputGroupAddon>
              <Icon
                as={MagnifyingGlassIcon}
                weight="bold"
                size={16}
                color={colors.mutedForeground}
              />
            </InputGroupAddon>
            <InputGroupInput
              value={query}
              onChangeText={setQuery}
              placeholder="Search"
              accessibilityLabel="Search settings"
              returnKeyType="search"
              autoCorrect={false}
              clearButtonMode="while-editing"
              style={{ fontSize: fontSize.lg.fontSize }}
            />
          </InputGroup>
          {query ? null : (
            <ItemGroup variant="inset" style={group}>
              <Item size="sm" onPress={onAccountPress} style={styles.account}>
                <Avatar size="xl" style={{ width: 58, height: 58 }}>
                  <AvatarFallback
                    style={{ backgroundColor: palette.indigo500 }}
                  >
                    <Text style={fontSize.xl}>JR</Text>
                  </AvatarFallback>
                </Avatar>
                <ItemContent style={{ gap: 2 }}>
                  <ItemTitle style={[fontSize.xl, { fontWeight: "600" }]}>
                    Jamie Rivera
                  </ItemTitle>
                  <ItemDescription style={fontSize.sm}>
                    suiss Account, Cloud and more
                  </ItemDescription>
                </ItemContent>
                <Chevron />
              </Item>
            </ItemGroup>
          )}
          {visible.map((rows) => (
            <ItemGroup key={rows[0].label} variant="inset" style={group}>
              {rows.map((row, index) => {
                const detail = row.detail?.(toggles)
                const toggle = row.toggle
                return (
                  <React.Fragment key={row.label}>
                    {index > 0 ? (
                      <ItemSeparator style={{ marginStart: 57 }} />
                    ) : null}
                    <Item
                      size="sm"
                      onPress={
                        toggle ? undefined : () => onRowPress?.(row.label)
                      }
                      style={styles.row}
                    >
                      <ItemMedia
                        style={[
                          styles.tile,
                          { backgroundColor: tone(colors, row.tint) },
                        ]}
                      >
                        <Icon
                          as={row.icon}
                          weight="bold"
                          size={17}
                          color={colors.primaryForeground}
                        />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle
                          nativeID={
                            toggle ? `settings-device-${toggle}` : undefined
                          }
                          style={{ fontWeight: "400" }}
                        >
                          {row.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions style={{ gap: 6 }}>
                        {toggle ? (
                          <Switch
                            checked={toggles[toggle]}
                            onCheckedChange={(value) =>
                              setToggle(toggle, value)
                            }
                            accessibilityLabelledBy={`settings-device-${toggle}`}
                          />
                        ) : (
                          <>
                            {detail ? <Text>{detail}</Text> : null}
                            <Chevron />
                          </>
                        )}
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                )
              })}
            </ItemGroup>
          ))}
          {visible.length === 0 ? (
            <Text style={[styles.empty, { color: colors.mutedForeground }]}>
              No Results
            </Text>
          ) : null}
        </View>
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsDevice }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/SettingsDevice.swift`: shown above

- `swift/Components/Avatar.swift`: see [Avatar](https://ui.suiss.app/docs/components/avatar.md)

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Header.swift`: see [Header](https://ui.suiss.app/docs/components/header.md)

- `swift/Components/InputGroup.swift`: see [Input Group](https://ui.suiss.app/docs/components/input-group.md)

- `swift/Components/Item.swift`: see [Item](https://ui.suiss.app/docs/components/item.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/Switch.swift`: see [Switch](https://ui.suiss.app/docs/components/switch.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/SettingsDevice.kt`: shown above

- `kotlin/components/Avatar.kt`: see [Avatar](https://ui.suiss.app/docs/components/avatar.md)

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Header.kt`: see [Header](https://ui.suiss.app/docs/components/header.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/InputGroup.kt`: see [Input Group](https://ui.suiss.app/docs/components/input-group.md)

- `kotlin/components/Item.kt`: see [Item](https://ui.suiss.app/docs/components/item.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `kotlin/components/Switch.kt`: see [Switch](https://ui.suiss.app/docs/components/switch.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { SettingsDevice } from "@/components/blocks/settings-device"
```

```tsx
<SettingsDevice />
```

Replace `GROUPS` with your rows and the account details with your user. Use `onAccountPress` and `onRowPress`, which receives the row label, to navigate.

