# Users Detail

A user directory with a member list beside a detail pane for profile, sessions, and security settings.

Users Detail 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/users-detail

**React**

**Example**

```tsx title="components/blocks/users-detail.tsx"
"use client"

import {
  ChartBarIcon,
  DesktopIcon,
  DeviceMobileIcon,
  GearIcon,
  GlobeIcon,
  ShieldIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
} from "@/components/patterns/split-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value: valueProp,
  defaultValue,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(
    defaultValue ?? items[0]?.value
  )
  const value = valueProp ?? uncontrolledValue
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          if (valueProp === undefined) {
                            setUncontrolledValue(item.value)
                          }
                          onValueChange?.(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const nav: NavItem[] = [
  {
    value: "users",
    label: "Users",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "roles",
    label: "Roles",
    icon: (
      <ShieldIcon />
    ),
  },
  {
    value: "log",
    label: "Activity",
    icon: (
      <ChartBarIcon />
    ),
  },
  {
    value: "settings",
    label: "Settings",
    icon: (
      <GearIcon />
    ),
  },
]

const roles = ["Admin", "Editor", "Support", "Viewer"]

const users = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    hue: 250,
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    hue: 30,
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    hue: 150,
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    hue: 200,
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    hue: 330,
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    hue: 100,
  },
]

const initialSessions = [
  {
    device: "Book Pro · Browser",
    place: "San Francisco, US",
    time: "Now",
    icon: (
      <DesktopIcon className="size-4" />
    ),
  },
  {
    device: "Phone Pro · App",
    place: "San Francisco, US",
    time: "2 hr ago",
    icon: (
      <DeviceMobileIcon className="size-4" />
    ),
  },
  {
    device: "Desktop · Browser",
    place: "Seattle, US",
    time: "3 days ago",
    icon: (
      <GlobeIcon className="size-4" />
    ),
  },
]

function UserAvatar({
  user,
  className,
}: {
  user: (typeof users)[number]
  className?: string
}) {
  return (
    <Avatar className={className}>
      <AvatarFallback
        style={{
          background: `linear-gradient(160deg, oklch(0.78 0.1 ${user.hue}), oklch(0.6 0.14 ${user.hue + 30}))`,
        }}
        className="text-white"
      >
        {user.name
          .split(" ")
          .map((part) => part[0])
          .join("")}
      </AvatarFallback>
    </Avatar>
  )
}

function SecurityToggle({
  label,
  initial,
}: {
  label: string
  initial: boolean
}) {
  const [on, setOn] = React.useState(initial)
  const id = React.useId()

  return (
    <Item size="sm" role="listitem">
      <ItemContent>
        <ItemTitle className="font-normal">
          <label htmlFor={id}>{label}</label>
        </ItemTitle>
      </ItemContent>
      <ItemActions>
        <Switch id={id} checked={on} onCheckedChange={setOn} />
      </ItemActions>
    </Item>
  )
}

export function UsersDetail() {
  const [selected, setSelected] = React.useState(0)
  const [tab, setTab] = React.useState("Profile")
  const [overrides, setOverrides] = React.useState<Record<number, string>>({})
  const [sessionsByUser, setSessionsByUser] = React.useState<
    Record<number, typeof initialSessions>
  >({})
  const user = users[selected]
  if (!user) return null
  const sessions = sessionsByUser[selected] ?? initialSessions
  const setSessions = (
    update: (current: typeof initialSessions) => typeof initialSessions
  ) =>
    setSessionsByUser((current) => ({
      ...current,
      [selected]: update(current[selected] ?? initialSessions),
    }))
  const role = overrides[selected] ?? user.role

  return (
    <SidebarProvider>
      <NavSidebar
        items={nav}
        defaultValue="users"
        header={
          <p className="flex items-center gap-2 px-2 pt-1 text-sm font-semibold">
            <span className="font-bold">suiss</span> Admin
          </p>
        }
      />
      <SidebarInset>
        <div className="flex items-center px-5 pt-4 md:hidden">
          <SidebarTrigger />
        </div>
        <SplitView
          defaultColumn="list"
          className="flex-1 rounded-none border-0 max-md:p-4 md:min-h-150"
        >
          <SplitViewList
            title="Users"
            className="gap-0.5 md:px-4 md:pt-5 [&>h2]:md:not-sr-only [&>h2]:md:px-1 [&>h2]:md:pb-2 [&>h2]:md:text-sm [&>h2]:md:font-semibold [&>h2]:md:text-muted-foreground"
          >
            {users.map((item, index) => (
              <SplitViewItem
                key={item.email}
                isActive={index === selected}
                onClick={() => {
                  setSelected(index)
                  setTab("Profile")
                }}
                className="group/user py-1.5 md:data-active:bg-primary md:data-active:text-primary-foreground"
              >
                <span className="flex items-center gap-2.5">
                  <UserAvatar user={item} className="size-7 *:text-xs" />
                  <span className="flex flex-col">
                    <span className="text-sm">{item.name}</span>
                    <span className="text-xs text-muted-foreground md:group-data-active/user:text-primary-foreground">
                      {overrides[index] ?? item.role}
                    </span>
                  </span>
                </span>
              </SplitViewItem>
            ))}
          </SplitViewList>
          <SplitViewDetail title={user.name} className="md:p-6 [&>h2]:sr-only">
            <div
              key={selected}
              className="flex flex-col gap-5 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
            >
              <div className="flex items-center gap-3.5">
                <UserAvatar user={user} className="size-16 *:text-2xl" />
                <div className="flex flex-col gap-0.5">
                  <p className="text-2xl font-bold">{user.name}</p>
                  <p className="text-sm text-muted-foreground">
                    {user.email} · {role}
                  </p>
                </div>
              </div>
              <ToggleGroup
                spacing={0}
                value={[tab]}
                onValueChange={(value: string[]) => {
                  if (value[0]) setTab(value[0])
                }}
                aria-label="Section"
              >
                {["Profile", "Sessions", "Security"].map((item) => (
                  <ToggleGroupItem
                    key={item}
                    value={item}
                    size="sm"
                    className="px-4"
                  >
                    {item}
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <div
                key={tab}
                className="flex flex-col gap-3 transition-[opacity,translate] duration-700 starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none"
              >
                {tab === "Profile" && (
                  <ItemGroup variant="inset">
                    {[
                      ["Full name", user.name],
                      ["Email", user.email],
                      ["Department", "Product"],
                      ["Joined", "March 12, 2024"],
                    ].map(([label, value]) => (
                      <React.Fragment key={label}>
                        <Item size="sm" role="listitem">
                          <ItemContent>
                            <ItemTitle className="font-normal">
                              {label}
                            </ItemTitle>
                          </ItemContent>
                          <ItemActions className="text-muted-foreground">
                            {value}
                          </ItemActions>
                        </Item>
                        <ItemSeparator />
                      </React.Fragment>
                    ))}
                    <Item size="sm" role="listitem">
                      <ItemContent>
                        <ItemTitle className="font-normal">Role</ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Select
                          value={role}
                          onValueChange={(value) =>
                            setOverrides((current) => ({
                              ...current,
                              [selected]: value as string,
                            }))
                          }
                        >
                          <SelectTrigger
                            size="sm"
                            aria-label="Role"
                            className="h-7.5"
                          >
                            <SelectValue />
                          </SelectTrigger>
                          <SelectContent align="end">
                            {roles.map((item) => (
                              <SelectItem key={item} value={item}>
                                {item}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                      </ItemActions>
                    </Item>
                    <ItemSeparator />
                    <Item size="sm" role="listitem">
                      <ItemContent>
                        <ItemTitle className="font-normal">Status</ItemTitle>
                      </ItemContent>
                      <ItemActions className="text-muted-foreground">
                        {user.status}
                      </ItemActions>
                    </Item>
                  </ItemGroup>
                )}
                {tab === "Sessions" && (
                  <>
                    <ItemGroup
                      variant="inset"
                      className="**:data-[slot=item-separator]:ms-15"
                    >
                      {sessions.map((session, index) => (
                        <React.Fragment key={session.device}>
                          {index > 0 && <ItemSeparator />}
                          <Item size="sm" role="listitem">
                            <ItemMedia className="size-7.5 rounded-[7px] bg-zinc-500 text-white">
                              {session.icon}
                            </ItemMedia>
                            <ItemContent className="gap-0">
                              <ItemTitle className="font-normal">
                                {session.device}
                              </ItemTitle>
                              <ItemDescription className="text-xs">
                                {session.place} · {session.time}
                              </ItemDescription>
                            </ItemContent>
                            <ItemActions>
                              {index === 0 ? (
                                <span className="text-sm">This device</span>
                              ) : (
                                <Button
                                  size="sm"
                                  variant="secondary"
                                  onClick={() =>
                                    setSessions((current) =>
                                      current.filter(
                                        (item) => item.device !== session.device
                                      )
                                    )
                                  }
                                >
                                  Sign Out
                                </Button>
                              )}
                            </ItemActions>
                          </Item>
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    {sessions.length > 1 && (
                      <Button
                        size="sm"
                        variant="destructive"
                        className="self-start"
                        onClick={() =>
                          setSessions((current) => current.slice(0, 1))
                        }
                      >
                        Sign Out All Other Sessions
                      </Button>
                    )}
                  </>
                )}
                {tab === "Security" && (
                  <>
                    <ItemGroup variant="inset" key={selected}>
                      <SecurityToggle
                        label="Two-factor authentication"
                        initial
                      />
                      <ItemSeparator />
                      <SecurityToggle label="Require passkey" initial={false} />
                      <ItemSeparator />
                      <SecurityToggle label="Sign-in alerts" initial />
                    </ItemGroup>
                    <Button
                      size="sm"
                      variant="destructive"
                      className="self-start"
                    >
                      Send Password Reset Link
                    </Button>
                  </>
                )}
              </div>
            </div>
          </SplitViewDetail>
        </SplitView>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

A user directory with members on the left and the selected user's profile, sessions and security settings on the right, switched with tabs. Admins can change a role, sign out sessions, toggle security options and send a password reset link. It combines `Sidebar` navigation with a `SplitView`, so on mobile the list and detail show one at a time. Use it for team members, customers or any record with tabbed details.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/users-detail
```

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 users-detail
```

### 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), [Button](https://ui.suiss.app/docs/components/button), [Item](https://ui.suiss.app/docs/components/item), [Select](https://ui.suiss.app/docs/components/select), [Sidebar](https://ui.suiss.app/docs/components/sidebar), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Switch](https://ui.suiss.app/docs/components/switch), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/users-detail.tsx`:

```tsx title="components/blocks/users-detail.tsx"
"use client"

import {
  ChartBarIcon,
  DesktopIcon,
  DeviceMobileIcon,
  GearIcon,
  GlobeIcon,
  ShieldIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
} from "@/components/patterns/split-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value: valueProp,
  defaultValue,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(
    defaultValue ?? items[0]?.value
  )
  const value = valueProp ?? uncontrolledValue
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          if (valueProp === undefined) {
                            setUncontrolledValue(item.value)
                          }
                          onValueChange?.(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const nav: NavItem[] = [
  {
    value: "users",
    label: "Users",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "roles",
    label: "Roles",
    icon: (
      <ShieldIcon />
    ),
  },
  {
    value: "log",
    label: "Activity",
    icon: (
      <ChartBarIcon />
    ),
  },
  {
    value: "settings",
    label: "Settings",
    icon: (
      <GearIcon />
    ),
  },
]

const roles = ["Admin", "Editor", "Support", "Viewer"]

const users = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    hue: 250,
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    hue: 30,
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    hue: 150,
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    hue: 200,
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    hue: 330,
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    hue: 100,
  },
]

const initialSessions = [
  {
    device: "Book Pro · Browser",
    place: "San Francisco, US",
    time: "Now",
    icon: (
      <DesktopIcon className="size-4" />
    ),
  },
  {
    device: "Phone Pro · App",
    place: "San Francisco, US",
    time: "2 hr ago",
    icon: (
      <DeviceMobileIcon className="size-4" />
    ),
  },
  {
    device: "Desktop · Browser",
    place: "Seattle, US",
    time: "3 days ago",
    icon: (
      <GlobeIcon className="size-4" />
    ),
  },
]

function UserAvatar({
  user,
  className,
}: {
  user: (typeof users)[number]
  className?: string
}) {
  return (
    <Avatar className={className}>
      <AvatarFallback
        style={{
          background: `linear-gradient(160deg, oklch(0.78 0.1 ${user.hue}), oklch(0.6 0.14 ${user.hue + 30}))`,
        }}
        className="text-white"
      >
        {user.name
          .split(" ")
          .map((part) => part[0])
          .join("")}
      </AvatarFallback>
    </Avatar>
  )
}

function SecurityToggle({
  label,
  initial,
}: {
  label: string
  initial: boolean
}) {
  const [on, setOn] = React.useState(initial)
  const id = React.useId()

  return (
    <Item size="sm" role="listitem">
      <ItemContent>
        <ItemTitle className="font-normal">
          <label htmlFor={id}>{label}</label>
        </ItemTitle>
      </ItemContent>
      <ItemActions>
        <Switch id={id} checked={on} onCheckedChange={setOn} />
      </ItemActions>
    </Item>
  )
}

export function UsersDetail() {
  const [selected, setSelected] = React.useState(0)
  const [tab, setTab] = React.useState("Profile")
  const [overrides, setOverrides] = React.useState<Record<number, string>>({})
  const [sessionsByUser, setSessionsByUser] = React.useState<
    Record<number, typeof initialSessions>
  >({})
  const user = users[selected]
  if (!user) return null
  const sessions = sessionsByUser[selected] ?? initialSessions
  const setSessions = (
    update: (current: typeof initialSessions) => typeof initialSessions
  ) =>
    setSessionsByUser((current) => ({
      ...current,
      [selected]: update(current[selected] ?? initialSessions),
    }))
  const role = overrides[selected] ?? user.role

  return (
    <SidebarProvider>
      <NavSidebar
        items={nav}
        defaultValue="users"
        header={
          <p className="flex items-center gap-2 px-2 pt-1 text-sm font-semibold">
            <span className="font-bold">suiss</span> Admin
          </p>
        }
      />
      <SidebarInset>
        <div className="flex items-center px-5 pt-4 md:hidden">
          <SidebarTrigger />
        </div>
        <SplitView
          defaultColumn="list"
          className="flex-1 rounded-none border-0 max-md:p-4 md:min-h-150"
        >
          <SplitViewList
            title="Users"
            className="gap-0.5 md:px-4 md:pt-5 [&>h2]:md:not-sr-only [&>h2]:md:px-1 [&>h2]:md:pb-2 [&>h2]:md:text-sm [&>h2]:md:font-semibold [&>h2]:md:text-muted-foreground"
          >
            {users.map((item, index) => (
              <SplitViewItem
                key={item.email}
                isActive={index === selected}
                onClick={() => {
                  setSelected(index)
                  setTab("Profile")
                }}
                className="group/user py-1.5 md:data-active:bg-primary md:data-active:text-primary-foreground"
              >
                <span className="flex items-center gap-2.5">
                  <UserAvatar user={item} className="size-7 *:text-xs" />
                  <span className="flex flex-col">
                    <span className="text-sm">{item.name}</span>
                    <span className="text-xs text-muted-foreground md:group-data-active/user:text-primary-foreground">
                      {overrides[index] ?? item.role}
                    </span>
                  </span>
                </span>
              </SplitViewItem>
            ))}
          </SplitViewList>
          <SplitViewDetail title={user.name} className="md:p-6 [&>h2]:sr-only">
            <div
              key={selected}
              className="flex flex-col gap-5 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
            >
              <div className="flex items-center gap-3.5">
                <UserAvatar user={user} className="size-16 *:text-2xl" />
                <div className="flex flex-col gap-0.5">
                  <p className="text-2xl font-bold">{user.name}</p>
                  <p className="text-sm text-muted-foreground">
                    {user.email} · {role}
                  </p>
                </div>
              </div>
              <ToggleGroup
                spacing={0}
                value={[tab]}
                onValueChange={(value: string[]) => {
                  if (value[0]) setTab(value[0])
                }}
                aria-label="Section"
              >
                {["Profile", "Sessions", "Security"].map((item) => (
                  <ToggleGroupItem
                    key={item}
                    value={item}
                    size="sm"
                    className="px-4"
                  >
                    {item}
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <div
                key={tab}
                className="flex flex-col gap-3 transition-[opacity,translate] duration-700 starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none"
              >
                {tab === "Profile" && (
                  <ItemGroup variant="inset">
                    {[
                      ["Full name", user.name],
                      ["Email", user.email],
                      ["Department", "Product"],
                      ["Joined", "March 12, 2024"],
                    ].map(([label, value]) => (
                      <React.Fragment key={label}>
                        <Item size="sm" role="listitem">
                          <ItemContent>
                            <ItemTitle className="font-normal">
                              {label}
                            </ItemTitle>
                          </ItemContent>
                          <ItemActions className="text-muted-foreground">
                            {value}
                          </ItemActions>
                        </Item>
                        <ItemSeparator />
                      </React.Fragment>
                    ))}
                    <Item size="sm" role="listitem">
                      <ItemContent>
                        <ItemTitle className="font-normal">Role</ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Select
                          value={role}
                          onValueChange={(value) =>
                            setOverrides((current) => ({
                              ...current,
                              [selected]: value as string,
                            }))
                          }
                        >
                          <SelectTrigger
                            size="sm"
                            aria-label="Role"
                            className="h-7.5"
                          >
                            <SelectValue />
                          </SelectTrigger>
                          <SelectContent align="end">
                            {roles.map((item) => (
                              <SelectItem key={item} value={item}>
                                {item}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                      </ItemActions>
                    </Item>
                    <ItemSeparator />
                    <Item size="sm" role="listitem">
                      <ItemContent>
                        <ItemTitle className="font-normal">Status</ItemTitle>
                      </ItemContent>
                      <ItemActions className="text-muted-foreground">
                        {user.status}
                      </ItemActions>
                    </Item>
                  </ItemGroup>
                )}
                {tab === "Sessions" && (
                  <>
                    <ItemGroup
                      variant="inset"
                      className="**:data-[slot=item-separator]:ms-15"
                    >
                      {sessions.map((session, index) => (
                        <React.Fragment key={session.device}>
                          {index > 0 && <ItemSeparator />}
                          <Item size="sm" role="listitem">
                            <ItemMedia className="size-7.5 rounded-[7px] bg-zinc-500 text-white">
                              {session.icon}
                            </ItemMedia>
                            <ItemContent className="gap-0">
                              <ItemTitle className="font-normal">
                                {session.device}
                              </ItemTitle>
                              <ItemDescription className="text-xs">
                                {session.place} · {session.time}
                              </ItemDescription>
                            </ItemContent>
                            <ItemActions>
                              {index === 0 ? (
                                <span className="text-sm">This device</span>
                              ) : (
                                <Button
                                  size="sm"
                                  variant="secondary"
                                  onClick={() =>
                                    setSessions((current) =>
                                      current.filter(
                                        (item) => item.device !== session.device
                                      )
                                    )
                                  }
                                >
                                  Sign Out
                                </Button>
                              )}
                            </ItemActions>
                          </Item>
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    {sessions.length > 1 && (
                      <Button
                        size="sm"
                        variant="destructive"
                        className="self-start"
                        onClick={() =>
                          setSessions((current) => current.slice(0, 1))
                        }
                      >
                        Sign Out All Other Sessions
                      </Button>
                    )}
                  </>
                )}
                {tab === "Security" && (
                  <>
                    <ItemGroup variant="inset" key={selected}>
                      <SecurityToggle
                        label="Two-factor authentication"
                        initial
                      />
                      <ItemSeparator />
                      <SecurityToggle label="Require passkey" initial={false} />
                      <ItemSeparator />
                      <SecurityToggle label="Sign-in alerts" initial />
                    </ItemGroup>
                    <Button
                      size="sm"
                      variant="destructive"
                      className="self-start"
                    >
                      Send Password Reset Link
                    </Button>
                  </>
                )}
              </div>
            </div>
          </SplitViewDetail>
        </SplitView>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { UsersDetail } from "@/components/blocks/users-detail"
```

```tsx
<UsersDetail />
```

Replace `users`, `roles` and `initialSessions` with your data. In the sample every user shares one session list, and Department and Joined are hard-coded, so load these per user. Connect the role change, session sign-outs, security switches and reset button to your user management API.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/users-detail.tsx"
import {
  ChartBarIcon,
  DesktopIcon,
  DeviceMobileIcon,
  GearIcon,
  GlobeIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { useWindowDimensions, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
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 { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "users", label: "Users", icon: UsersIcon },
  { value: "roles", label: "Roles", icon: ShieldIcon },
  { value: "log", label: "Activity", icon: ChartBarIcon },
  { value: "settings", label: "Settings", icon: GearIcon },
]

const ROLES = ["Admin", "Editor", "Support", "Viewer"]

const TABS = ["Profile", "Sessions", "Security"]

type User = {
  name: string
  email: string
  role: string
  status: string
  tint: string
}

const USERS: User[] = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    tint: "bg-indigo-500",
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    tint: "bg-orange-500",
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    tint: "bg-green-500",
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    tint: "bg-cyan-500",
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    tint: "bg-pink-500",
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    tint: "bg-teal-400",
  },
]

type Session = {
  device: string
  place: string
  time: string
  icon: PhosphorIcon
}

const INITIAL_SESSIONS: Session[] = [
  {
    device: "Book Pro · Browser",
    place: "San Francisco, US",
    time: "Now",
    icon: DesktopIcon,
  },
  {
    device: "Phone Pro · App",
    place: "San Francisco, US",
    time: "2 hr ago",
    icon: DeviceMobileIcon,
  },
  {
    device: "Desktop · Browser",
    place: "Seattle, US",
    time: "3 days ago",
    icon: GlobeIcon,
  },
]

const SECURITY = [
  { label: "Two-factor authentication", initial: true },
  { label: "Require passkey", initial: false },
  { label: "Sign-in alerts", initial: true },
]

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function AdminTabBar({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <TabBar value={value} onValueChange={(next) => onValueChange(next)}>
      <TabBarList>
        {SECTIONS.map((section) => (
          <TabBarItem
            key={section.value}
            value={section.value}
            icon={section.icon}
          >
            {section.label}
          </TabBarItem>
        ))}
      </TabBarList>
    </TabBar>
  )
}

function OtherSection({ value, home }: { value: string; home: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-13">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        {`This section isn't part of the sample. Choose ${home}.`}
      </Text>
    </View>
  )
}

function UserAvatar({
  user,
  size,
  className,
}: {
  user: User
  size: "sm" | "xl"
  className?: string
}) {
  return (
    <Avatar size={size} className={className}>
      <AvatarFallback className={user.tint}>
        {initials(user.name)}
      </AvatarFallback>
    </Avatar>
  )
}

function SecurityToggle({
  label,
  initial,
  onChange,
}: {
  label: string
  initial: boolean
  onChange: (enabled: boolean) => void
}) {
  const [on, setOn] = React.useState(initial)
  const id = `users-detail-${label.toLowerCase().replaceAll(" ", "-")}`

  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle nativeID={id} className="font-normal">
          {label}
        </ItemTitle>
      </ItemContent>
      <ItemActions>
        <Switch
          checked={on}
          onCheckedChange={(value) => {
            setOn(value)
            onChange(value)
          }}
          accessibilityLabelledBy={id}
        />
      </ItemActions>
    </Item>
  )
}

function ValueRow({ label, value }: { label: string; value: string }) {
  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle className="font-normal">{label}</ItemTitle>
      </ItemContent>
      <ItemActions className="shrink">
        <Text numberOfLines={1}>{value}</Text>
      </ItemActions>
    </Item>
  )
}

function UsersDetail({
  onRoleChange,
  onSignOut,
  onSignOutAll,
  onSecurityChange,
  onSendPasswordReset,
  onNavigate,
}: {
  onRoleChange?: (email: string, role: string) => void
  onSignOut?: (device: string) => void
  onSignOutAll?: () => void
  onSecurityChange?: (email: string, setting: string, enabled: boolean) => void
  onSendPasswordReset?: (email: string) => void
  onNavigate?: (section: string) => void
}) {
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [section, setSection] = React.useState("users")
  const [selected, setSelected] = React.useState(0)
  const [tab, setTab] = React.useState("Profile")
  const [overrides, setOverrides] = React.useState<Record<number, string>>({})
  const [sessions, setSessions] = React.useState(INITIAL_SESSIONS)
  const user = USERS[selected] ?? USERS[0]
  if (!user) return null
  const role = overrides[selected] ?? user.role

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "users" ? (
        <SplitView className={wide ? "m-4" : undefined}>
          <SplitViewList
            title="Users"
            className={wide ? "gap-0.5 px-4 pt-5" : "px-4 pb-8"}
          >
            {wide ? (
              <Text
                role="heading"
                className="px-1 pb-2 text-sm font-semibold text-muted-foreground"
              >
                Users
              </Text>
            ) : null}
            {USERS.map((item, index) => {
              const active = wide && index === selected
              return (
                <SplitViewItem
                  key={item.email}
                  isActive={index === selected}
                  onPress={() => {
                    setSelected(index)
                    setTab("Profile")
                  }}
                  className={active ? "bg-primary py-1.5" : "py-1.5"}
                >
                  <View className="flex-row items-center gap-2.5">
                    <UserAvatar user={item} size="sm" className="size-7" />
                    <View className="min-w-0 flex-1">
                      <Text
                        numberOfLines={1}
                        className={
                          active ? "text-sm text-primary-foreground" : "text-sm"
                        }
                      >
                        {item.name}
                      </Text>
                      <Text
                        className={
                          active
                            ? "text-xs text-primary-foreground"
                            : "text-xs text-muted-foreground"
                        }
                      >
                        {overrides[index] ?? item.role}
                      </Text>
                    </View>
                  </View>
                </SplitViewItem>
              )
            })}
          </SplitViewList>
          <SplitViewDetail className={wide ? "p-6" : "px-4 pb-8"}>
            <AnimatedView
              key={selected}
              entering={FadeInDown.duration(800).reduceMotion(
                ReduceMotion.System
              )}
              className="gap-5"
            >
              <View className="flex-row items-center gap-3.5">
                <UserAvatar user={user} size="xl" />
                <View className="min-w-0 flex-1 gap-0.5">
                  <Text role="heading" className="text-2xl font-bold">
                    {user.name}
                  </Text>
                  <Text className="text-sm text-muted-foreground">
                    {`${user.email} · ${role}`}
                  </Text>
                </View>
              </View>
              <ToggleGroup
                spacing={0}
                size="sm"
                value={[tab]}
                onValueChange={(value) => {
                  if (value[0]) setTab(value[0])
                }}
                accessibilityLabel="Section"
              >
                {TABS.map((item) => (
                  <ToggleGroupItem key={item} value={item} className="px-4">
                    <Text>{item}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <AnimatedView
                key={tab}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
                className="gap-3"
              >
                {tab === "Profile" ? (
                  <ItemGroup variant="inset">
                    {[
                      ["Full name", user.name],
                      ["Email", user.email],
                      ["Department", "Product"],
                      ["Joined", "March 12, 2024"],
                    ].map(([label = "", value = ""]) => (
                      <React.Fragment key={label}>
                        <ValueRow label={label} value={value} />
                        <ItemSeparator />
                      </React.Fragment>
                    ))}
                    <Item size="sm">
                      <ItemContent>
                        <ItemTitle className="font-normal">Role</ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Picker
                          size="sm"
                          value={role}
                          onValueChange={(value) => {
                            onRoleChange?.(user.email, value)
                            setOverrides((current) => ({
                              ...current,
                              [selected]: value,
                            }))
                          }}
                          accessibilityLabel="Role"
                          className="bg-transparent px-1"
                        >
                          {ROLES.map((item) => (
                            <PickerItem key={item} value={item}>
                              {item}
                            </PickerItem>
                          ))}
                        </Picker>
                      </ItemActions>
                    </Item>
                    <ItemSeparator />
                    <ValueRow label="Status" value={user.status} />
                  </ItemGroup>
                ) : null}
                {tab === "Sessions" ? (
                  <>
                    <ItemGroup variant="inset">
                      {sessions.map((session, index) => (
                        <React.Fragment key={session.device}>
                          {index > 0 ? (
                            <ItemSeparator className="ms-15" />
                          ) : null}
                          <Item size="sm">
                            <ItemMedia className="size-7.5 rounded-[7px] bg-zinc-500">
                              <Icon
                                as={session.icon}
                                weight="bold"
                                className="size-4 text-primary-foreground"
                              />
                            </ItemMedia>
                            <ItemContent className="gap-0">
                              <ItemTitle className="font-normal">
                                {session.device}
                              </ItemTitle>
                              <ItemDescription className="text-xs">
                                {`${session.place} · ${session.time}`}
                              </ItemDescription>
                            </ItemContent>
                            <ItemActions>
                              {index === 0 ? (
                                <Text className="text-sm text-foreground">
                                  This device
                                </Text>
                              ) : (
                                <Button
                                  size="sm"
                                  variant="secondary"
                                  onPress={() => {
                                    onSignOut?.(session.device)
                                    setSessions((current) =>
                                      current.filter(
                                        (item) => item.device !== session.device
                                      )
                                    )
                                  }}
                                >
                                  <Text>Sign Out</Text>
                                </Button>
                              )}
                            </ItemActions>
                          </Item>
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    {sessions.length > 1 ? (
                      <Button
                        size="sm"
                        variant="destructive"
                        className="self-start"
                        onPress={() => {
                          onSignOutAll?.()
                          setSessions((current) => current.slice(0, 1))
                        }}
                      >
                        <Text>Sign Out All Other Sessions</Text>
                      </Button>
                    ) : null}
                  </>
                ) : null}
                {tab === "Security" ? (
                  <>
                    <ItemGroup key={selected} variant="inset">
                      {SECURITY.map((setting, index) => (
                        <React.Fragment key={setting.label}>
                          {index > 0 ? <ItemSeparator /> : null}
                          <SecurityToggle
                            label={setting.label}
                            initial={setting.initial}
                            onChange={(enabled) =>
                              onSecurityChange?.(
                                user.email,
                                setting.label,
                                enabled
                              )
                            }
                          />
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    <Button
                      size="sm"
                      variant="destructive"
                      className="self-start"
                      onPress={() => onSendPasswordReset?.(user.email)}
                    >
                      <Text>Send Password Reset Link</Text>
                    </Button>
                  </>
                ) : null}
              </AnimatedView>
            </AnimatedView>
          </SplitViewDetail>
        </SplitView>
      ) : (
        <OtherSection value={section} home="Users" />
      )}
      <AdminTabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      />
    </Screen>
  )
}

export { UsersDetail }
```

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

public struct UsersDetail: View {
  struct User: Identifiable {
    let name: String
    let email: String
    let role: String
    let status: String
    let tint: Color

    var id: String { email }
  }

  struct Session: Identifiable, Equatable {
    let device: String
    let place: String
    let time: String
    let icon: Image

    var id: String { device }
  }

  static let roles = ["Admin", "Editor", "Support", "Viewer"]
  static let tabs = ["Profile", "Sessions", "Security"]

  static var users: [User] {
    [
      User(name: "Jamie Rivera", email: "jamie@example.com", role: "Admin", status: "Active", tint: Palette.indigo500),
      User(name: "Morgan Lee", email: "morgan@example.com", role: "Editor", status: "Active", tint: Palette.orange500),
      User(name: "Riley Chen", email: "riley@example.com", role: "Support", status: "Active", tint: Palette.green500),
      User(name: "Taylor Kim", email: "taylor@example.com", role: "Viewer", status: "Suspended", tint: Palette.blue500),
      User(name: "Avery Brooks", email: "avery@example.com", role: "Editor", status: "Invited", tint: Palette.pink500),
      User(name: "Jordan Park", email: "jordan@example.com", role: "Support", status: "Active", tint: Palette.teal400),
    ]
  }

  static var initialSessions: [Session] {
    [
      Session(device: "Book Pro · Browser", place: "San Francisco, US", time: "Now", icon: Ph.desktop.bold),
      Session(device: "Phone Pro · App", place: "San Francisco, US", time: "2 hr ago", icon: Ph.deviceMobile.bold),
      Session(device: "Desktop · Browser", place: "Seattle, US", time: "3 days ago", icon: Ph.globe.bold),
    ]
  }

  static let security: [(label: LocalizedStringKey, key: String, initial: Bool)] = [
    ("Two-factor authentication", "twoFactor", true),
    ("Require passkey", "passkey", false),
    ("Sign-in alerts", "alerts", true),
  ]

  private var onNavigate: ((String) -> Void)?
  private var onRoleChange: ((String, String) -> Void)?
  private var onSignOut: ((String) -> Void)?
  private var onSignOutAll: (() -> Void)?
  private var onSecurityChange: ((String, String, Bool) -> Void)?
  private var onSendPasswordReset: ((String) -> Void)?
  @State private var selected: Int
  @State private var tab: String?
  @State private var overrides: [Int: String] = [:]
  @State private var sessions: [Session]
  @State private var settings: [Int: [String: Bool]] = [:]
  @State private var column: SplitViewColumn
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onRoleChange: ((_ user: String, _ role: String) -> Void)? = nil,
    onSignOut: ((_ device: String) -> Void)? = nil,
    onSignOutAll: (() -> Void)? = nil,
    onSecurityChange: ((_ user: String, _ setting: String, _ enabled: Bool) -> Void)? = nil,
    onSendPasswordReset: ((_ user: String) -> Void)? = nil,
    onNavigate: ((_ section: String) -> Void)? = nil
  ) {
    self.init(selected: 0)
    self.onRoleChange = onRoleChange
    self.onSignOut = onSignOut
    self.onSignOutAll = onSignOutAll
    self.onNavigate = onNavigate
    self.onSecurityChange = onSecurityChange
    self.onSendPasswordReset = onSendPasswordReset
  }

  init(selected: Int, tab: String = "Profile", column: SplitViewColumn = .list) {
    _selected = State(initialValue: selected)
    _tab = State(initialValue: tab)
    _column = State(initialValue: column)
    _sessions = State(initialValue: Self.initialSessions)
  }

  private var animation: Animation? {
    reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.45)
  }

  private func role(_ index: Int) -> String {
    overrides[index] ?? Self.users[index].role
  }

  public var body: some View {
    UsersDetailShell(home: "users", onNavigate: onNavigate) {
      page
    }
  }

  @ViewBuilder private var page: some View {
    let user = Self.users[selected]
    Screen {
      SplitView(column: $column) {
        SplitViewList("Users") {
          VStack(alignment: .leading, spacing: 0) {
            UsersDetailHeading()
            ForEach(Array(Self.users.enumerated()), id: \.element.id) { index, item in
              SplitViewItem(isActive: index == selected, action: {
                selected = index
                tab = "Profile"
              }) {
                HStack(spacing: 10) {
                  UsersDetailAvatar(user: item, size: 32)
                  VStack(alignment: .leading, spacing: 0) {
                    Text(verbatim: item.name)
                      .scaledFont(.sm)
                    Text(LocalizedStringKey(role(index)))
                      .scaledFont(.xs)
                      .foregroundStyle(EsuissColor.mutedForeground)
                  }
                }
                .accessibilityElement(children: .combine)
              }
            }
          }
        }
        SplitViewDetail {
          detail(user)
            .id(selected)
            .transition(.opacity.combined(with: .offset(y: 8)))
        }
      }
    }
  }

  private func detail(_ user: User) -> some View {
    let current = tab ?? "Profile"
    return VStack(alignment: .leading, spacing: 20) {
      HStack(spacing: 14) {
        UsersDetailAvatar(user: user, size: 64)
        VStack(alignment: .leading, spacing: 2) {
          Text(verbatim: user.name)
            .scaledTextStyle(.xl2, weight: .bold)
            .accessibilityAddTraits(.isHeader)
          Text(verbatim: "\(user.email) · \(role(selected))")
            .scaledFont(.sm)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
      }
      .padding(.top, 8)
      ToggleGroup(
        selection: Binding<String?>(
          get: { tab },
          set: { value in
            guard let value else { return }
            withAnimation(animation) { tab = value }
          }
        ),
        size: .sm,
        spacing: 0
      ) {
        ForEach(Self.tabs, id: \.self) { item in
          ToggleGroupItem(LocalizedStringKey(item), value: item)
            .frame(maxWidth: .infinity)
        }
      }
      .accessibilityLabel(Text("Section"))
      Group {
        switch current {
        case "Sessions": sessionsTab(user)
        case "Security": securityTab(user)
        default: profileTab(user)
        }
      }
      .id(current)
      .transition(.opacity.combined(with: .offset(y: 6)))
    }
    .foregroundStyle(EsuissColor.foreground)
  }

  private func profileTab(_ user: User) -> some View {
    let rows: [(LocalizedStringKey, String)] = [
      ("Full name", user.name),
      ("Email", user.email),
      ("Department", "Product"),
      ("Joined", "March 12, 2024"),
    ]
    let index = selected
    let binding = Binding<String?>(
      get: { role(index) },
      set: { value in
        guard let value, value != role(index) else { return }
        onRoleChange?(user.email, value)
        overrides[index] = value
      }
    )
    return ItemGroup(.inset) {
      ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
        Item(size: .sm) {
          ItemContent {
            ItemTitle { Text(row.0).fontWeight(.regular) }
          }
          ItemActions {
            Text(verbatim: row.1)
              .foregroundStyle(EsuissColor.mutedForeground)
              .lineLimit(1)
          }
        }
        .accessibilityElement(children: .combine)
        ItemSeparator()
      }
      Item(size: .sm) {
        ItemContent {
          ItemTitle { Text("Role").fontWeight(.regular) }
        }
        ItemActions {
          OptionPicker(selection: binding, size: .sm) {
            ForEach(Self.roles, id: \.self) { item in
              OptionPickerItem(LocalizedStringKey(item), value: item)
            }
          }
          .fixedSize()
          .accessibilityLabel(Text("Role"))
        }
      }
      ItemSeparator()
      Item(size: .sm) {
        ItemContent {
          ItemTitle { Text("Status").fontWeight(.regular) }
        }
        ItemActions {
          Text(LocalizedStringKey(user.status))
            .foregroundStyle(EsuissColor.mutedForeground)
        }
      }
      .accessibilityElement(children: .combine)
    }
  }

  private func sessionsTab(_ user: User) -> some View {
    VStack(alignment: .leading, spacing: 12) {
      ItemGroup(.inset) {
        ForEach(Array(sessions.enumerated()), id: \.element.id) { index, session in
          if index > 0 {
            ItemSeparator().padding(.leading, 44)
          }
          Item(size: .sm) {
            ItemMedia {
              session.icon
                .frame(width: 16, height: 16)
                .foregroundStyle(EsuissColor.primaryForeground)
                .frame(width: 30, height: 30)
                .background(Palette.zinc500, in: RoundedRectangle(cornerRadius: 7, style: .continuous))
            }
            ItemContent {
              ItemTitle { Text(verbatim: session.device).fontWeight(.regular) }
              ItemDescription(verbatim: "\(session.place) · \(session.time)")
            }
            ItemActions {
              if index == 0 {
                Text("This device")
                  .scaledFont(.sm)
              } else {
                Button("Sign Out") {
                  onSignOut?(session.device)
                  withAnimation(animation) { sessions.removeAll { $0.device == session.device } }
                }
                .buttonStyle(.variant(.secondary, size: .sm))
              }
            }
          }
          .transition(.opacity)
        }
      }
      if sessions.count > 1 {
        Button("Sign Out All Other Sessions") {
          onSignOutAll?()
          withAnimation(animation) { sessions = Array(sessions.prefix(1)) }
        }
        .buttonStyle(.variant(.destructive, size: .sm))
      }
    }
  }

  private func securityTab(_ user: User) -> some View {
    let index = selected
    return VStack(alignment: .leading, spacing: 12) {
      ItemGroup(.inset) {
        ForEach(Array(Self.security.enumerated()), id: \.offset) { position, item in
          if position > 0 {
            ItemSeparator()
          }
          Item(size: .sm) {
            ItemContent {
              ItemTitle { Text(item.label).fontWeight(.regular) }
            }
            ItemActions {
              Toggle(isOn: Binding(
                get: { settings[index]?[item.key] ?? item.initial },
                set: { value in
                  onSecurityChange?(user.email, item.key, value)
                  settings[index, default: [:]][item.key] = value
                }
              )) {
                Text(item.label)
              }
              .labelsHidden()
              .toggleStyle(SwitchStyle())
              .accessibilityLabel(Text(item.label))
            }
          }
        }
      }
      Button("Send Password Reset Link") {
        onSendPasswordReset?(user.email)
      }
      .buttonStyle(.variant(.destructive, size: .sm))
    }
  }
}

private struct UsersDetailAvatar: View {
  let user: UsersDetail.User
  let size: CGFloat

  var body: some View {
    Text(verbatim: user.name.split(separator: " ").compactMap(\.first).map(String.init).joined())
      .font(.system(size: size * 0.38, weight: .semibold))
      .foregroundStyle(EsuissColor.primaryForeground)
      .frame(width: size, height: size)
      .background(user.tint, in: Circle())
      .accessibilityHidden(true)
  }
}

private struct UsersDetailHeading: View {
  @Environment(\.splitView) private var splitView

  var body: some View {
    if splitView?.wide ?? false {
      Text("Users")
        .scaledFont(.sm, weight: .semibold)
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.horizontal, 4)
        .padding(.vertical, 8)
        .accessibilityAddTraits(.isHeader)
    }
  }
}

private struct UsersDetailShell<Content: View>: View {
  let home: String
  let onNavigate: ((String) -> Void)?
  let content: Content

  @State private var tab: String

  init(home: String, onNavigate: ((String) -> Void)?, @ViewBuilder content: () -> Content) {
    self.home = home
    self.onNavigate = onNavigate
    self.content = content()
    _tab = State(initialValue: home)
  }

  private static var sections: [(value: String, label: LocalizedStringKey, icon: Image, active: Image)] {
    [
      ("users", "Users", Ph.users.regular, Ph.users.fill),
      ("roles", "Roles", Ph.shield.regular, Ph.shield.fill),
      ("log", "Activity", Ph.chartBar.regular, Ph.chartBar.fill),
      ("settings", "Settings", Ph.gear.regular, Ph.gear.fill),
    ]
  }

  var body: some View {
    let selection = Binding<String>(
      get: { tab },
      set: { value in
        guard value != tab else { return }
        onNavigate?(value)
        tab = value
      }
    )
    Screen {
      ZStack {
        content
          .opacity(tab == home ? 1 : 0)
          .allowsHitTesting(tab == home)
          .accessibilityHidden(tab != home)
        if tab != home {
          Empty {
            EmptyHeader {
              EmptyDescription("This section isn't part of the sample. Choose \(Text(Self.sections.first { $0.value == home }?.label ?? "")).")
            }
          }
          .background(EsuissColor.background)
        }
      }
      .frame(maxHeight: .infinity)
      TabBar(selection: selection) {
        ForEach(Self.sections, id: \.value) { section in
          TabBarItem(section.label, value: section.value, icon: section.icon, activeIcon: section.active)
        }
      }
    }
  }
}
```

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

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
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.Row
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
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.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.bold.Desktop
import com.adamglin.phosphoricons.bold.DeviceMobile
import com.adamglin.phosphoricons.bold.Globe
import com.adamglin.phosphoricons.fill.ChartBar as FillChartBar
import com.adamglin.phosphoricons.fill.Gear as FillGear
import com.adamglin.phosphoricons.fill.Shield as FillShield
import com.adamglin.phosphoricons.fill.Users as FillUsers
import com.adamglin.phosphoricons.regular.ChartBar
import com.adamglin.phosphoricons.regular.Gear
import com.adamglin.phosphoricons.regular.Shield
import com.adamglin.phosphoricons.regular.Users
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Item
import com.esuiss.ui.components.ItemActions
import com.esuiss.ui.components.ItemContent
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.ScreenEdge
import com.esuiss.ui.components.SelectSize
import com.esuiss.ui.components.Switch
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.patterns.Picker
import com.esuiss.ui.patterns.PickerItemData
import com.esuiss.ui.patterns.SplitView
import com.esuiss.ui.patterns.SplitViewColumn
import com.esuiss.ui.patterns.SplitViewItem
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography

private val DetailRevealEasing = CubicBezierEasing(0.32f, 0.72f, 0f, 1f)

private class DetailUser(val name: String, val email: String, val role: String, val status: String, val hue: Int)

private val DetailUsers = listOf(
    DetailUser("Jamie Rivera", "jamie@example.com", "Admin", "Active", 250),
    DetailUser("Morgan Lee", "morgan@example.com", "Editor", "Active", 30),
    DetailUser("Riley Chen", "riley@example.com", "Support", "Active", 150),
    DetailUser("Taylor Kim", "taylor@example.com", "Viewer", "Suspended", 200),
    DetailUser("Avery Brooks", "avery@example.com", "Editor", "Invited", 330),
    DetailUser("Jordan Park", "jordan@example.com", "Support", "Active", 100),
)

private val DetailRoles = listOf("Admin", "Editor", "Support", "Viewer").map { PickerItemData(it, it) }

private class DetailSession(val device: String, val place: String, val time: String, val icon: ImageVector)

private fun detailSessions() = listOf(
    DetailSession("Book Pro · Browser", "San Francisco, US", "Now", PhosphorIcons.Bold.Desktop),
    DetailSession("Phone Pro · App", "San Francisco, US", "2 hr ago", PhosphorIcons.Bold.DeviceMobile),
    DetailSession("Desktop · Browser", "Seattle, US", "3 days ago", PhosphorIcons.Bold.Globe),
)

private val DetailSecurity = listOf(
    "Two-factor authentication" to true,
    "Require passkey" to false,
    "Sign-in alerts" to true,
)

private fun detailTint(colors: Colors, hue: Int): Color = when (hue) {
    250 -> Palette.indigo500
    30 -> Palette.orange500
    150 -> Palette.green500
    200 -> Palette.blue500
    330 -> Palette.pink500
    else -> Palette.teal500
}

@Composable
fun UsersDetail(
    modifier: Modifier = Modifier,
    onRoleChange: (email: String, role: String) -> Unit = { _, _ -> },
    onSignOut: (device: String) -> Unit = {},
    onSignOutAll: () -> Unit = {},
    onSecurityChange: (email: String, setting: String, enabled: Boolean) -> Unit = { _, _, _ -> },
    onSendPasswordReset: (email: String) -> Unit = {},
    onNavigate: (section: String) -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var selected by rememberSaveable { mutableStateOf(0) }
    var tab by rememberSaveable { mutableStateOf("Profile") }
    var column by rememberSaveable { mutableStateOf(SplitViewColumn.List) }
    val overrides = remember { mutableStateMapOf<Int, String>() }
    val security = remember { mutableStateMapOf<String, Boolean>() }
    val sessions = remember { mutableStateListOf<DetailSession>().apply { addAll(detailSessions()) } }
    val user = DetailUsers[selected]
    val role = overrides[selected] ?: user.role

    Screen(modifier, edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        AdminTabs("users", onNavigate) {
            SplitView(column = column, onColumnChange = { column = it }) {
                SplitViewList(title = "Users") {
                    DetailUsers.forEachIndexed { index, item ->
                        SplitViewItem(
                            isActive = index == selected,
                            onClick = {
                                selected = index
                                tab = "Profile"
                            },
                        ) {
                            Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
                                DetailAvatar(item, 28.dp)
                                Column {
                                    BasicText(item.name, style = Typography.sm.copy(color = colors.foreground))
                                    BasicText(overrides[index] ?: item.role, style = Typography.xs.copy(color = colors.mutedForeground))
                                }
                            }
                        }
                    }
                }
                SplitViewDetail {
                    key(selected) {
                        DetailAppear {
                            Column(
                                modifier = Modifier.padding(top = 8.dp),
                                verticalArrangement = Arrangement.spacedBy(20.dp),
                            ) {
                                Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
                                    DetailAvatar(user, 64.dp)
                                    Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                                        BasicText(
                                            user.name,
                                            modifier = Modifier.semantics { heading() },
                                            style = Typography.xl.copy(color = colors.foreground, fontWeight = FontWeight.Bold),
                                        )
                                        BasicText("${user.email} · $role", style = Typography.sm.copy(color = colors.mutedForeground))
                                    }
                                }
                                ToggleGroup(
                                    value = tab,
                                    onValueChange = { if (it != null) tab = it },
                                    modifier = Modifier.semantics { contentDescription = "Section" },
                                    size = ToggleSize.Sm,
                                    spacing = 0,
                                ) {
                                    listOf("Profile", "Sessions", "Security").forEach { ToggleGroupItem(it, it) }
                                }
                                key(tab) {
                                    DetailAppear {
                                        Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                                            when (tab) {
                                                "Profile" -> DetailProfile(user, role) { value ->
                                                    onRoleChange(user.email, value)
                                                    overrides[selected] = value
                                                }
                                                "Sessions" -> DetailSessions(
                                                    sessions = sessions,
                                                    onSignOut = { session ->
                                                        onSignOut(session.device)
                                                        sessions.remove(session)
                                                    },
                                                    onSignOutAll = {
                                                        onSignOutAll()
                                                        while (sessions.size > 1) sessions.removeAt(sessions.lastIndex)
                                                    },
                                                )
                                                else -> {
                                                    ItemGroup(variant = ItemGroupVariant.Inset) {
                                                        DetailSecurity.forEachIndexed { index, (label, initial) ->
                                                            if (index > 0) ItemSeparator()
                                                            val keyName = "$selected-$label"
                                                            DetailRow(label) {
                                                                Switch(
                                                                    checked = security[keyName] ?: initial,
                                                                    onCheckedChange = {
                                                                        onSecurityChange(user.email, label, it)
                                                                        security[keyName] = it
                                                                    },
                                                                    modifier = Modifier.semantics { contentDescription = label },
                                                                )
                                                            }
                                                        }
                                                    }
                                                    Button(
                                                        text = "Send Password Reset Link",
                                                        onClick = { onSendPasswordReset(user.email) },
                                                        size = ButtonSize.Sm,
                                                        variant = ButtonVariant.Destructive,
                                                    )
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun DetailAppear(content: @Composable () -> Unit) {
    val appear = remember { Animatable(0f) }
    LaunchedEffect(Unit) { appear.animateTo(1f, tween(800, easing = DetailRevealEasing)) }
    Box(
        Modifier.graphicsLayer {
            alpha = appear.value
            translationY = (1f - appear.value) * 8.dp.toPx()
        },
    ) {
        content()
    }
}

@Composable
private fun DetailAvatar(user: DetailUser, size: Dp) {
    val colors = EsuissTheme.colors
    Box(
        Modifier
            .size(size)
            .background(detailTint(colors, user.hue), CircleShape),
        contentAlignment = Alignment.Center,
    ) {
        BasicText(
            user.name.split(" ").mapNotNull { it.firstOrNull() }.joinToString(""),
            style = Typography.xs.copy(color = Palette.white, fontWeight = FontWeight.Medium, fontSize = (size.value * 0.38f).sp),
        )
    }
}

@Composable
private fun DetailRow(title: String, action: @Composable () -> Unit) {
    Item(size = ItemSize.Sm, role = null) {
        ItemContent {
            ItemTitle {
                BasicText(
                    title,
                    style = Typography.base.copy(color = EsuissTheme.colors.foreground),
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
        ItemActions { action() }
    }
}

@Composable
private fun DetailProfile(user: DetailUser, role: String, onRole: (String) -> Unit) {
    val colors = EsuissTheme.colors
    ItemGroup(variant = ItemGroupVariant.Inset) {
        listOf(
            "Full name" to user.name,
            "Email" to user.email,
            "Department" to "Product",
            "Joined" to "March 12, 2024",
        ).forEach { (label, value) ->
            DetailRow(label) { BasicText(value, style = Typography.base.copy(color = colors.mutedForeground), maxLines = 1) }
            ItemSeparator()
        }
        DetailRow("Role") {
            Picker(
                value = role,
                onValueChange = onRole,
                modifier = Modifier.semantics { contentDescription = "Role" },
                items = DetailRoles,
                size = SelectSize.Sm,
            )
        }
        ItemSeparator()
        DetailRow("Status") { BasicText(user.status, style = Typography.base.copy(color = colors.mutedForeground)) }
    }
}

@Composable
private fun DetailSessions(
    sessions: List<DetailSession>,
    onSignOut: (DetailSession) -> Unit,
    onSignOutAll: () -> Unit,
) {
    val colors = EsuissTheme.colors
    ItemGroup(variant = ItemGroupVariant.Inset) {
        sessions.forEachIndexed { index, session ->
            key(session.device) {
                if (index > 0) ItemSeparator(Modifier.padding(start = 40.dp))
                Item(size = ItemSize.Sm, role = null) {
                    ItemMedia {
                        Box(
                            Modifier
                                .size(30.dp)
                                .background(Palette.zinc500, RoundedCornerShape(7.dp)),
                            contentAlignment = Alignment.Center,
                        ) {
                            Icon(session.icon, contentDescription = null, size = 16.dp, tint = Palette.white)
                        }
                    }
                    ItemContent {
                        BasicText(
                            session.device,
                            style = Typography.base.copy(color = colors.foreground),
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis,
                        )
                        BasicText(
                            "${session.place} · ${session.time}",
                            style = Typography.xs.copy(color = colors.mutedForeground),
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis,
                        )
                    }
                    ItemActions {
                        if (index == 0) {
                            BasicText("This device", style = Typography.sm.copy(color = colors.foreground))
                        } else {
                            Button(text = "Sign Out", onClick = { onSignOut(session) }, size = ButtonSize.Sm, variant = ButtonVariant.Secondary)
                        }
                    }
                }
            }
        }
    }
    if (sessions.size > 1) {
        Button(
            text = "Sign Out All Other Sessions",
            onClick = onSignOutAll,
            size = ButtonSize.Sm,
            variant = ButtonVariant.Destructive,
        )
    }
}

private val AdminSections = listOf("users" to "Users", "roles" to "Roles", "log" to "Activity", "settings" to "Settings")

@Composable
private fun AdminTabs(home: String, onNavigate: (String) -> Unit, content: @Composable () -> Unit) {
    val colors = EsuissTheme.colors
    var tab by rememberSaveable { mutableStateOf(home) }
    Column(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .weight(1f)
                .fillMaxWidth(),
        ) {
            if (tab == home) {
                content()
            } else {
                BasicText(
                    "This section isn't part of the sample. Choose ${AdminSections.first { it.first == home }.second}.",
                    modifier = Modifier
                        .align(Alignment.Center)
                        .padding(32.dp),
                    style = Typography.base.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                )
            }
        }
        TabBar(
            value = tab,
            onValueChange = {
                onNavigate(it)
                tab = it
            },
        ) {
            TabBarItem("users", "Users", PhosphorIcons.Regular.Users, selectedIcon = PhosphorIcons.Fill.FillUsers)
            TabBarItem("roles", "Roles", PhosphorIcons.Regular.Shield, selectedIcon = PhosphorIcons.Fill.FillShield)
            TabBarItem("log", "Activity", PhosphorIcons.Regular.ChartBar, selectedIcon = PhosphorIcons.Fill.FillChartBar)
            TabBarItem("settings", "Settings", PhosphorIcons.Regular.Gear, selectedIcon = PhosphorIcons.Fill.FillGear)
        }
    }
}
```

A `SplitView` lists users with an `Avatar` and status, and the detail pane shows the selected user's header and a `ToggleGroup` for Profile, Sessions and Security. Profile lists details with a role `Picker`, Sessions lets you sign out one device or all others, and Security has `Switch` rows and a password reset `Button`. The admin `TabBar` switches sections.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/users-detail
```

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 users-detail
```

**SwiftUI**

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

**Jetpack Compose**

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

The CLI also adds what it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Switch](https://ui.suiss.app/docs/components/switch), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group). On React Native it also adds the animated view `components/ui/animated-view.tsx`.

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Switch](https://ui.suiss.app/docs/components/switch), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `uniwind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/uniwind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/users-detail.tsx`:

- `uniwind/components/blocks/users-detail.tsx`: shown above

5. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Switch](https://ui.suiss.app/docs/components/switch), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `nativewind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/nativewind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/users-detail.tsx`:

```tsx title="nativewind/components/blocks/users-detail.tsx"
import {
  ChartBarIcon,
  DesktopIcon,
  DeviceMobileIcon,
  GearIcon,
  GlobeIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { useWindowDimensions, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
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 { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "users", label: "Users", icon: UsersIcon },
  { value: "roles", label: "Roles", icon: ShieldIcon },
  { value: "log", label: "Activity", icon: ChartBarIcon },
  { value: "settings", label: "Settings", icon: GearIcon },
]

const ROLES = ["Admin", "Editor", "Support", "Viewer"]

const TABS = ["Profile", "Sessions", "Security"]

type User = {
  name: string
  email: string
  role: string
  status: string
  tint: string
}

const USERS: User[] = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    tint: "bg-indigo-500",
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    tint: "bg-orange-500",
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    tint: "bg-green-500",
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    tint: "bg-cyan-500",
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    tint: "bg-pink-500",
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    tint: "bg-teal-400",
  },
]

type Session = {
  device: string
  place: string
  time: string
  icon: PhosphorIcon
}

const INITIAL_SESSIONS: Session[] = [
  {
    device: "Book Pro · Browser",
    place: "San Francisco, US",
    time: "Now",
    icon: DesktopIcon,
  },
  {
    device: "Phone Pro · App",
    place: "San Francisco, US",
    time: "2 hr ago",
    icon: DeviceMobileIcon,
  },
  {
    device: "Desktop · Browser",
    place: "Seattle, US",
    time: "3 days ago",
    icon: GlobeIcon,
  },
]

const SECURITY = [
  { label: "Two-factor authentication", initial: true },
  { label: "Require passkey", initial: false },
  { label: "Sign-in alerts", initial: true },
]

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function AdminTabBar({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <TabBar value={value} onValueChange={(next) => onValueChange(next)}>
      <TabBarList>
        {SECTIONS.map((section) => (
          <TabBarItem
            key={section.value}
            value={section.value}
            icon={section.icon}
          >
            {section.label}
          </TabBarItem>
        ))}
      </TabBarList>
    </TabBar>
  )
}

function OtherSection({ value, home }: { value: string; home: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-[3.25rem]">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        {`This section isn't part of the sample. Choose ${home}.`}
      </Text>
    </View>
  )
}

function UserAvatar({
  user,
  size,
  className,
}: {
  user: User
  size: "sm" | "xl"
  className?: string
}) {
  return (
    <Avatar size={size} className={className}>
      <AvatarFallback className={user.tint}>
        {initials(user.name)}
      </AvatarFallback>
    </Avatar>
  )
}

function SecurityToggle({
  label,
  initial,
  onChange,
}: {
  label: string
  initial: boolean
  onChange: (enabled: boolean) => void
}) {
  const [on, setOn] = React.useState(initial)
  const id = `users-detail-${label.toLowerCase().replaceAll(" ", "-")}`

  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle nativeID={id} className="font-normal">
          {label}
        </ItemTitle>
      </ItemContent>
      <ItemActions>
        <Switch
          checked={on}
          onCheckedChange={(value) => {
            setOn(value)
            onChange(value)
          }}
          accessibilityLabelledBy={id}
        />
      </ItemActions>
    </Item>
  )
}

function ValueRow({ label, value }: { label: string; value: string }) {
  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle className="font-normal">{label}</ItemTitle>
      </ItemContent>
      <ItemActions className="shrink">
        <Text numberOfLines={1}>{value}</Text>
      </ItemActions>
    </Item>
  )
}

function UsersDetail({
  onRoleChange,
  onSignOut,
  onSignOutAll,
  onSecurityChange,
  onSendPasswordReset,
  onNavigate,
}: {
  onRoleChange?: (email: string, role: string) => void
  onSignOut?: (device: string) => void
  onSignOutAll?: () => void
  onSecurityChange?: (email: string, setting: string, enabled: boolean) => void
  onSendPasswordReset?: (email: string) => void
  onNavigate?: (section: string) => void
}) {
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [section, setSection] = React.useState("users")
  const [selected, setSelected] = React.useState(0)
  const [tab, setTab] = React.useState("Profile")
  const [overrides, setOverrides] = React.useState<Record<number, string>>({})
  const [sessions, setSessions] = React.useState(INITIAL_SESSIONS)
  const user = USERS[selected] ?? USERS[0]
  if (!user) return null
  const role = overrides[selected] ?? user.role

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "users" ? (
        <SplitView className={wide ? "m-4" : undefined}>
          <SplitViewList
            title="Users"
            className={wide ? "gap-0.5 px-4 pt-5" : "px-4 pb-8"}
          >
            {wide ? (
              <Text
                role="heading"
                className="px-1 pb-2 text-sm font-semibold text-muted-foreground"
              >
                Users
              </Text>
            ) : null}
            {USERS.map((item, index) => {
              const active = wide && index === selected
              return (
                <SplitViewItem
                  key={item.email}
                  isActive={index === selected}
                  onPress={() => {
                    setSelected(index)
                    setTab("Profile")
                  }}
                  className={active ? "bg-primary py-1.5" : "py-1.5"}
                >
                  <View className="flex-row items-center gap-2.5">
                    <UserAvatar user={item} size="sm" className="size-7" />
                    <View className="min-w-0 flex-1">
                      <Text
                        numberOfLines={1}
                        className={
                          active ? "text-sm text-primary-foreground" : "text-sm"
                        }
                      >
                        {item.name}
                      </Text>
                      <Text
                        className={
                          active
                            ? "text-xs text-primary-foreground"
                            : "text-xs text-muted-foreground"
                        }
                      >
                        {overrides[index] ?? item.role}
                      </Text>
                    </View>
                  </View>
                </SplitViewItem>
              )
            })}
          </SplitViewList>
          <SplitViewDetail className={wide ? "p-6" : "px-4 pb-8"}>
            <AnimatedView
              key={selected}
              entering={FadeInDown.duration(800).reduceMotion(
                ReduceMotion.System
              )}
              className="gap-5"
            >
              <View className="flex-row items-center gap-3.5">
                <UserAvatar user={user} size="xl" />
                <View className="min-w-0 flex-1 gap-0.5">
                  <Text role="heading" className="text-2xl font-bold">
                    {user.name}
                  </Text>
                  <Text className="text-sm text-muted-foreground">
                    {`${user.email} · ${role}`}
                  </Text>
                </View>
              </View>
              <ToggleGroup
                spacing={0}
                size="sm"
                value={[tab]}
                onValueChange={(value) => {
                  if (value[0]) setTab(value[0])
                }}
                accessibilityLabel="Section"
              >
                {TABS.map((item) => (
                  <ToggleGroupItem key={item} value={item} className="px-4">
                    <Text>{item}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <AnimatedView
                key={tab}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
                className="gap-3"
              >
                {tab === "Profile" ? (
                  <ItemGroup variant="inset">
                    {[
                      ["Full name", user.name],
                      ["Email", user.email],
                      ["Department", "Product"],
                      ["Joined", "March 12, 2024"],
                    ].map(([label = "", value = ""]) => (
                      <React.Fragment key={label}>
                        <ValueRow label={label} value={value} />
                        <ItemSeparator />
                      </React.Fragment>
                    ))}
                    <Item size="sm">
                      <ItemContent>
                        <ItemTitle className="font-normal">Role</ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Picker
                          size="sm"
                          value={role}
                          onValueChange={(value) => {
                            onRoleChange?.(user.email, value)
                            setOverrides((current) => ({
                              ...current,
                              [selected]: value,
                            }))
                          }}
                          accessibilityLabel="Role"
                          className="bg-transparent px-1"
                        >
                          {ROLES.map((item) => (
                            <PickerItem key={item} value={item}>
                              {item}
                            </PickerItem>
                          ))}
                        </Picker>
                      </ItemActions>
                    </Item>
                    <ItemSeparator />
                    <ValueRow label="Status" value={user.status} />
                  </ItemGroup>
                ) : null}
                {tab === "Sessions" ? (
                  <>
                    <ItemGroup variant="inset">
                      {sessions.map((session, index) => (
                        <React.Fragment key={session.device}>
                          {index > 0 ? (
                            <ItemSeparator className="ms-[3.75rem]" />
                          ) : null}
                          <Item size="sm">
                            <ItemMedia className="size-[1.875rem] rounded-[7px] bg-zinc-500">
                              <Icon
                                as={session.icon}
                                weight="bold"
                                className="size-4 text-primary-foreground"
                              />
                            </ItemMedia>
                            <ItemContent className="gap-0">
                              <ItemTitle className="font-normal">
                                {session.device}
                              </ItemTitle>
                              <ItemDescription className="text-xs">
                                {`${session.place} · ${session.time}`}
                              </ItemDescription>
                            </ItemContent>
                            <ItemActions>
                              {index === 0 ? (
                                <Text className="text-sm text-foreground">
                                  This device
                                </Text>
                              ) : (
                                <Button
                                  size="sm"
                                  variant="secondary"
                                  onPress={() => {
                                    onSignOut?.(session.device)
                                    setSessions((current) =>
                                      current.filter(
                                        (item) => item.device !== session.device
                                      )
                                    )
                                  }}
                                >
                                  <Text>Sign Out</Text>
                                </Button>
                              )}
                            </ItemActions>
                          </Item>
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    {sessions.length > 1 ? (
                      <Button
                        size="sm"
                        variant="destructive"
                        className="self-start"
                        onPress={() => {
                          onSignOutAll?.()
                          setSessions((current) => current.slice(0, 1))
                        }}
                      >
                        <Text>Sign Out All Other Sessions</Text>
                      </Button>
                    ) : null}
                  </>
                ) : null}
                {tab === "Security" ? (
                  <>
                    <ItemGroup key={selected} variant="inset">
                      {SECURITY.map((setting, index) => (
                        <React.Fragment key={setting.label}>
                          {index > 0 ? <ItemSeparator /> : null}
                          <SecurityToggle
                            label={setting.label}
                            initial={setting.initial}
                            onChange={(enabled) =>
                              onSecurityChange?.(
                                user.email,
                                setting.label,
                                enabled
                              )
                            }
                          />
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    <Button
                      size="sm"
                      variant="destructive"
                      className="self-start"
                      onPress={() => onSendPasswordReset?.(user.email)}
                    >
                      <Text>Send Password Reset Link</Text>
                    </Button>
                  </>
                ) : null}
              </AnimatedView>
            </AnimatedView>
          </SplitViewDetail>
        </SplitView>
      ) : (
        <OtherSection value={section} home="Users" />
      )}
      <AdminTabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      />
    </Screen>
  )
}

export { UsersDetail }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Switch](https://ui.suiss.app/docs/components/switch), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `stylesheet/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/stylesheet/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/users-detail.tsx`:

```tsx title="stylesheet/components/blocks/users-detail.tsx"
import {
  ChartBarIcon,
  DesktopIcon,
  DeviceMobileIcon,
  GearIcon,
  GlobeIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { StyleSheet, useWindowDimensions, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
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 { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Tone = keyof ThemeColors | keyof typeof palette

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

const SECTIONS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "users", label: "Users", icon: UsersIcon },
  { value: "roles", label: "Roles", icon: ShieldIcon },
  { value: "log", label: "Activity", icon: ChartBarIcon },
  { value: "settings", label: "Settings", icon: GearIcon },
]

const ROLES = ["Admin", "Editor", "Support", "Viewer"]

const TABS = ["Profile", "Sessions", "Security"]

type Tint = Tone

type User = {
  name: string
  email: string
  role: string
  status: string
  tint: Tint
}

const USERS: User[] = [
  {
    name: "Jamie Rivera",
    email: "jamie@example.com",
    role: "Admin",
    status: "Active",
    tint: "indigo500",
  },
  {
    name: "Morgan Lee",
    email: "morgan@example.com",
    role: "Editor",
    status: "Active",
    tint: "orange500",
  },
  {
    name: "Riley Chen",
    email: "riley@example.com",
    role: "Support",
    status: "Active",
    tint: "green500",
  },
  {
    name: "Taylor Kim",
    email: "taylor@example.com",
    role: "Viewer",
    status: "Suspended",
    tint: "cyan500",
  },
  {
    name: "Avery Brooks",
    email: "avery@example.com",
    role: "Editor",
    status: "Invited",
    tint: "pink500",
  },
  {
    name: "Jordan Park",
    email: "jordan@example.com",
    role: "Support",
    status: "Active",
    tint: "teal400",
  },
]

type Session = {
  device: string
  place: string
  time: string
  icon: PhosphorIcon
}

const INITIAL_SESSIONS: Session[] = [
  {
    device: "Book Pro · Browser",
    place: "San Francisco, US",
    time: "Now",
    icon: DesktopIcon,
  },
  {
    device: "Phone Pro · App",
    place: "San Francisco, US",
    time: "2 hr ago",
    icon: DeviceMobileIcon,
  },
  {
    device: "Desktop · Browser",
    place: "Seattle, US",
    time: "3 days ago",
    icon: GlobeIcon,
  },
]

const SECURITY = [
  { label: "Two-factor authentication", initial: true },
  { label: "Require passkey", initial: false },
  { label: "Sign-in alerts", initial: true },
]

const styles = StyleSheet.create({
  other: { flex: 1, gap: 8, paddingHorizontal: 16, paddingTop: 52 },
  otherTitle: { ...fontSize["3xl"], fontWeight: "700" },
  wideRoot: { margin: 16 },
  listWide: { gap: 2, paddingHorizontal: 16, paddingTop: 20 },
  compact: { paddingHorizontal: 16, paddingBottom: 32 },
  detailWide: { padding: 24 },
  listHeading: {
    ...fontSize.sm,
    paddingHorizontal: 4,
    paddingBottom: 8,
    fontWeight: "600",
  },
  listItem: { paddingVertical: 6 },
  listRow: { flexDirection: "row", alignItems: "center", gap: 10 },
  listText: { minWidth: 0, flex: 1 },
  smallAvatar: { width: 28, height: 28 },
  detail: { gap: 20 },
  profile: { flexDirection: "row", alignItems: "center", gap: 14 },
  profileText: { minWidth: 0, flex: 1, gap: 2 },
  name: { ...fontSize["2xl"], fontWeight: "700" },
  tab: { paddingHorizontal: 16 },
  panel: { gap: 12 },
  regular: { fontWeight: "400" },
  shrink: { flexShrink: 1 },
  picker: { backgroundColor: "transparent", paddingHorizontal: 4 },
  sessionSeparator: { marginStart: 60 },
  sessionMedia: { width: 30, height: 30, borderRadius: 7 },
  tight: { gap: 0 },
  start: { alignSelf: "flex-start" },
})

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function AdminTabBar({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <TabBar value={value} onValueChange={(next) => onValueChange(next)}>
      <TabBarList>
        {SECTIONS.map((section) => (
          <TabBarItem
            key={section.value}
            value={section.value}
            icon={section.icon}
          >
            {section.label}
          </TabBarItem>
        ))}
      </TabBarList>
    </TabBar>
  )
}

function OtherSection({ value, home }: { value: string; home: string }) {
  const { colors } = useTheme()
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View style={styles.other}>
      <Text role="heading" style={styles.otherTitle}>
        {label}
      </Text>
      <Text style={[fontSize.lg, { color: colors.mutedForeground }]}>
        {`This section isn't part of the sample. Choose ${home}.`}
      </Text>
    </View>
  )
}

function UserAvatar({
  user,
  size,
  style,
}: {
  user: User
  size: "sm" | "xl"
  style?: React.ComponentProps<typeof Avatar>["style"]
}) {
  const { colors } = useTheme()
  return (
    <Avatar size={size} style={style}>
      <AvatarFallback style={{ backgroundColor: tone(colors, user.tint) }}>
        {initials(user.name)}
      </AvatarFallback>
    </Avatar>
  )
}

function SecurityToggle({
  label,
  initial,
  onChange,
}: {
  label: string
  initial: boolean
  onChange: (enabled: boolean) => void
}) {
  const [on, setOn] = React.useState(initial)
  const id = `users-detail-${label.toLowerCase().replaceAll(" ", "-")}`

  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle nativeID={id} style={styles.regular}>
          {label}
        </ItemTitle>
      </ItemContent>
      <ItemActions>
        <Switch
          checked={on}
          onCheckedChange={(value) => {
            setOn(value)
            onChange(value)
          }}
          accessibilityLabelledBy={id}
        />
      </ItemActions>
    </Item>
  )
}

function ValueRow({ label, value }: { label: string; value: string }) {
  return (
    <Item size="sm">
      <ItemContent>
        <ItemTitle style={styles.regular}>{label}</ItemTitle>
      </ItemContent>
      <ItemActions style={styles.shrink}>
        <Text numberOfLines={1}>{value}</Text>
      </ItemActions>
    </Item>
  )
}

function UsersDetail({
  onRoleChange,
  onSignOut,
  onSignOutAll,
  onSecurityChange,
  onSendPasswordReset,
  onNavigate,
}: {
  onRoleChange?: (email: string, role: string) => void
  onSignOut?: (device: string) => void
  onSignOutAll?: () => void
  onSecurityChange?: (email: string, setting: string, enabled: boolean) => void
  onSendPasswordReset?: (email: string) => void
  onNavigate?: (section: string) => void
}) {
  const { colors } = useTheme()
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [section, setSection] = React.useState("users")
  const [selected, setSelected] = React.useState(0)
  const [tab, setTab] = React.useState("Profile")
  const [overrides, setOverrides] = React.useState<Record<number, string>>({})
  const [sessions, setSessions] = React.useState(INITIAL_SESSIONS)
  const user = USERS[selected] ?? USERS[0]
  if (!user) return null
  const role = overrides[selected] ?? user.role

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "users" ? (
        <SplitView style={wide ? styles.wideRoot : undefined}>
          <SplitViewList
            title="Users"
            style={wide ? styles.listWide : styles.compact}
          >
            {wide ? (
              <Text
                role="heading"
                style={[styles.listHeading, { color: colors.mutedForeground }]}
              >
                Users
              </Text>
            ) : null}
            {USERS.map((item, index) => {
              const active = wide && index === selected
              return (
                <SplitViewItem
                  key={item.email}
                  isActive={index === selected}
                  onPress={() => {
                    setSelected(index)
                    setTab("Profile")
                  }}
                  style={
                    active
                      ? { paddingVertical: 6, backgroundColor: colors.primary }
                      : styles.listItem
                  }
                >
                  <View style={styles.listRow}>
                    <UserAvatar
                      user={item}
                      size="sm"
                      style={styles.smallAvatar}
                    />
                    <View style={styles.listText}>
                      <Text
                        numberOfLines={1}
                        style={[
                          fontSize.sm,
                          active && { color: colors.primaryForeground },
                        ]}
                      >
                        {item.name}
                      </Text>
                      <Text
                        style={[
                          fontSize.xs,
                          {
                            color: active
                              ? colors.primaryForeground
                              : colors.mutedForeground,
                          },
                        ]}
                      >
                        {overrides[index] ?? item.role}
                      </Text>
                    </View>
                  </View>
                </SplitViewItem>
              )
            })}
          </SplitViewList>
          <SplitViewDetail style={wide ? styles.detailWide : styles.compact}>
            <AnimatedView
              key={selected}
              entering={FadeInDown.duration(800).reduceMotion(
                ReduceMotion.System
              )}
              style={styles.detail}
            >
              <View style={styles.profile}>
                <UserAvatar user={user} size="xl" />
                <View style={styles.profileText}>
                  <Text role="heading" style={styles.name}>
                    {user.name}
                  </Text>
                  <Text
                    style={[fontSize.sm, { color: colors.mutedForeground }]}
                  >
                    {`${user.email} · ${role}`}
                  </Text>
                </View>
              </View>
              <ToggleGroup
                spacing={0}
                size="sm"
                value={[tab]}
                onValueChange={(value) => {
                  if (value[0]) setTab(value[0])
                }}
                accessibilityLabel="Section"
              >
                {TABS.map((item) => (
                  <ToggleGroupItem key={item} value={item} style={styles.tab}>
                    <Text>{item}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <AnimatedView
                key={tab}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
                style={styles.panel}
              >
                {tab === "Profile" ? (
                  <ItemGroup variant="inset">
                    {[
                      ["Full name", user.name],
                      ["Email", user.email],
                      ["Department", "Product"],
                      ["Joined", "March 12, 2024"],
                    ].map(([label = "", value = ""]) => (
                      <React.Fragment key={label}>
                        <ValueRow label={label} value={value} />
                        <ItemSeparator />
                      </React.Fragment>
                    ))}
                    <Item size="sm">
                      <ItemContent>
                        <ItemTitle style={styles.regular}>Role</ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Picker
                          size="sm"
                          value={role}
                          onValueChange={(value) => {
                            onRoleChange?.(user.email, value)
                            setOverrides((current) => ({
                              ...current,
                              [selected]: value,
                            }))
                          }}
                          accessibilityLabel="Role"
                          style={styles.picker}
                        >
                          {ROLES.map((item) => (
                            <PickerItem key={item} value={item}>
                              {item}
                            </PickerItem>
                          ))}
                        </Picker>
                      </ItemActions>
                    </Item>
                    <ItemSeparator />
                    <ValueRow label="Status" value={user.status} />
                  </ItemGroup>
                ) : null}
                {tab === "Sessions" ? (
                  <>
                    <ItemGroup variant="inset">
                      {sessions.map((session, index) => (
                        <React.Fragment key={session.device}>
                          {index > 0 ? (
                            <ItemSeparator style={styles.sessionSeparator} />
                          ) : null}
                          <Item size="sm">
                            <ItemMedia
                              style={[
                                styles.sessionMedia,
                                { backgroundColor: palette.zinc500 },
                              ]}
                            >
                              <Icon
                                as={session.icon}
                                weight="bold"
                                size={16}
                                color={colors.primaryForeground}
                              />
                            </ItemMedia>
                            <ItemContent style={styles.tight}>
                              <ItemTitle style={styles.regular}>
                                {session.device}
                              </ItemTitle>
                              <ItemDescription style={fontSize.xs}>
                                {`${session.place} · ${session.time}`}
                              </ItemDescription>
                            </ItemContent>
                            <ItemActions>
                              {index === 0 ? (
                                <Text
                                  style={[
                                    fontSize.sm,
                                    { color: colors.foreground },
                                  ]}
                                >
                                  This device
                                </Text>
                              ) : (
                                <Button
                                  size="sm"
                                  variant="secondary"
                                  onPress={() => {
                                    onSignOut?.(session.device)
                                    setSessions((current) =>
                                      current.filter(
                                        (item) => item.device !== session.device
                                      )
                                    )
                                  }}
                                >
                                  <Text>Sign Out</Text>
                                </Button>
                              )}
                            </ItemActions>
                          </Item>
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    {sessions.length > 1 ? (
                      <Button
                        size="sm"
                        variant="destructive"
                        style={styles.start}
                        onPress={() => {
                          onSignOutAll?.()
                          setSessions((current) => current.slice(0, 1))
                        }}
                      >
                        <Text>Sign Out All Other Sessions</Text>
                      </Button>
                    ) : null}
                  </>
                ) : null}
                {tab === "Security" ? (
                  <>
                    <ItemGroup key={selected} variant="inset">
                      {SECURITY.map((setting, index) => (
                        <React.Fragment key={setting.label}>
                          {index > 0 ? <ItemSeparator /> : null}
                          <SecurityToggle
                            label={setting.label}
                            initial={setting.initial}
                            onChange={(enabled) =>
                              onSecurityChange?.(
                                user.email,
                                setting.label,
                                enabled
                              )
                            }
                          />
                        </React.Fragment>
                      ))}
                    </ItemGroup>
                    <Button
                      size="sm"
                      variant="destructive"
                      style={styles.start}
                      onPress={() => onSendPasswordReset?.(user.email)}
                    >
                      <Text>Send Password Reset Link</Text>
                    </Button>
                  </>
                ) : null}
              </AnimatedView>
            </AnimatedView>
          </SplitViewDetail>
        </SplitView>
      ) : (
        <OtherSection value={section} home="Users" />
      )}
      <AdminTabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      />
    </Screen>
  )
}

export { UsersDetail }
```

5. 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/UsersDetail.swift`: shown above

- `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/DropdownMenu.swift`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

- `swift/Components/Empty.swift`: see [Empty](https://ui.suiss.app/docs/components/empty.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/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/Overlay.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/Overlay.swift)

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

- `swift/Components/Select.swift`: see [Select](https://ui.suiss.app/docs/components/select.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/TabBar.swift`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.md)

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

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

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

- `swift/Components/ToggleGroup.swift`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

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

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

- `swift/Patterns/SplitView.swift`: see [Split View](https://ui.suiss.app/docs/patterns/split-view.md)

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/UsersDetail.kt`: shown above

- `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/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.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/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/Overlay.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Overlay.kt)

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

- `kotlin/components/Select.kt`: see [Select](https://ui.suiss.app/docs/components/select.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)

- `kotlin/components/TabBar.kt`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.md)

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

- `kotlin/components/ToggleGroup.kt`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

- `kotlin/patterns/Picker.kt`: see [Picker](https://ui.suiss.app/docs/patterns/picker.md)

- `kotlin/patterns/SplitView.kt`: see [Split View](https://ui.suiss.app/docs/patterns/split-view.md)

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

## Usage

```tsx
import { UsersDetail } from "@/components/blocks/users-detail"
```

```tsx
<UsersDetail />
```

Replace `USERS`, `ROLES` and `INITIAL_SESSIONS` with your data. Use `onRoleChange`, `onSignOut`, `onSignOutAll`, `onSecurityChange`, `onSendPasswordReset` and `onNavigate` to call your API.

