Mobile

Phone app parts on UI SDK: rows that swipe aside to actions (createSwipeActions), wheel pickers and a time wheel (bindScrollPicker), bottom sheets and action sheets (Drawer), pull to refresh, an app bar and a tab bar.

The parts of a phone app, on UI SDK's gesture behaviors and components. Every gesture has a keyboard path and a screen reader path, and every part works with a mouse too.

pnpm dlx shadcn@latest add superzero11/uiset/mobile

Parts

PartWhat it is
SwipeListRows that swipe aside to actions on either side; a long swipe runs the first (createSwipeActions)
PullToRefreshPull down at the top to refresh, with resistance (rubberBand); a Refresh button for the keyboard
WheelPickerA wheel you scroll to choose from (bindScrollPicker on a Listbox)
TimeWheelHour, minute and AM/PM wheels, as "HH:MM"
SheetA bottom sheet, as tall as its content or resting at snap points (Drawer)
ActionSheetA sheet of choices with Cancel; await sheet.choose() returns the one chosen
AppBar, TabBarThe top bar (back, title, actions) and the bottom bar (sections, the current one marked)

Use

tsx
function Inbox() {
  const [sheet] = useState(createActionSheet)
  const options = async () => {
    const choice = await sheet.choose() // "read-all", or undefined for Cancel
    if (choice === "read-all") markAllRead()
  }
  return (
    <>
      <AppBar title="Inbox" actions={<Button aria-label="Inbox options" onClick={() => void options()}>…</Button>} />
      <PullToRefresh onRefresh={() => fetchMail()} onError={report}>
        <SwipeList
          label="Messages"
          items={messages.map((m) => ({
            id: m.id,
            label: `${m.from}: ${m.subject}`,
            content: <MessageRow message={m} />,
            start: [{ id: "read", label: "Read", icon: "mail" }],
            end: [
              { id: "archive", label: "Archive", icon: "archive", tone: "neutral" },
              { id: "delete", label: "Delete", icon: "trash", tone: "danger" },
            ],
          }))}
          onOpen={open}
          onAction={(id, action) => run(id, action)}
        />
      </PullToRefresh>
      <ActionSheet controller={sheet} title="Inbox" actions={[{ value: "read-all", label: "Mark all as read" }]} />
      <TabBar value={tab} onValueChange={setTab}
        items={[{ id: "inbox", label: "Inbox", icon: "inbox", badge: 2 }, { id: "alarms", label: "Alarms", icon: "bell" }]} />
    </>
  )
}

<Sheet title="New alarm" open={open} onOpenChange={setOpen}>
  <TimeWheel label="Alarm time" value={time} onValueChange={setTime} />
</Sheet>

<Sheet title="Help" snapPoints={[0.5, 1]} trigger={<DrawerTrigger>Help</DrawerTrigger>}>…</Sheet>
  • start actions show by swiping toward the end (left to right in English), end actions the other way; it flips in right-to-left pages. A side's first action sits at the row's edge. Past fullSwipe (0.6 of the row) that action fills the row, and letting go runs it and announces it. Hidden actions are inert.
  • After an action, focus goes back to its row, or, if the row went (archived, deleted), to the row now in its place. A tap on an open row closes it instead of opening the row.
  • Each row with actions also has an Actions menu. It's out of the tab order (a row is one tab stop) and out of sight, but screen readers reach it with their own keys and swipes, where neither swipes nor arrows reach the actions; it shows while it has focus.
  • A row without label is named by its content's text.
  • PullToRefresh waits for its onRefresh Promise. A rejected one is announced ("Couldn't refresh.") and handed to onError. Pulls only start at the top and only downward, so the list still scrolls; a short pull springs back. The pull moves by a CSS variable, so a pull doesn't re-render the list.
  • A Sheet with one snap point (the default, "content") is as tall as its content and has a plain bar. With more it's 85% of the screen, UI SDK's limit for a drawer, and its handle is a button that steps through the points. Drawer's other props (dismissible, defaultOpen, modalAbove, …) pass through.
  • WheelPicker keeps focus on its value: when the value changes from outside, focus in the wheel moves with it, so nothing that reveals the focused option scrolls the wheel back. TimeWheel shows a value between its minuteSteps too (07 with a step of 5).
  • A TabBar item with href is a link, and a click with a modifier key is left to the browser (a new tab); without href it's a UI SDK Button. Badges are a number on screen and words in the name ("Inbox, 2 new").
  • AppBar takes headingLevel (1 by default) to fit the page's outline.
  • Accent text uses --mobile-accent, the primary pulled toward the text color so it clears 4.5:1 in every skin. Focus on a filled swipe action is drawn in its own text color.

Keys

PartKeys
SwipeListUp and Down between rows (one tab stop); Left and Right show a row's actions and focus the first; Escape closes them; Enter opens the row
PullToRefreshTab reaches a Refresh button at the top of the list
WheelPickerArrows, Home, End, Page Up and Down, and typing, like any Listbox
SheetEscape closes; the handle steps through snap points with Enter, or arrows, Home and End

Touch

Rows swipe with a finger without stopping the list from scrolling up and down (touch-action: pan-y). At the top of the list, a finger pulling down is the refresh's, and scrolling down stays the browser's. Nothing gets selected while a row swipes or the list pulls. Wheels snap to an option (scroll-snap-type).

For agents

  • Messages (the SwipeList's label): run(row, action) runs a row's action by names people see, like run("Ada Lovelace", "Archive"); openRow(row) opens one. Agents read the rows with query, each with its actions. A danger action isn't run this way: the step fails with "Needs a person" and points at the row's Actions menu, whose item carries data-agent-confirm, so pressing it there asks the person.
  • ActionSheet choices marked destructive carry data-agent-confirm too.
  • Tab bar sections, app bar buttons, wheels (setValue on "Alarm time, hour") and sheets are UI SDK components agents use by name.

Other languages

Every word the parts say is a prop: SwipeList messages (help, actionsFor), PullToRefresh messages (refresh, pull, release, refreshing, done, failed), TimeWheel labels (hour, minute, period, am, pm), Sheet resizeLabel, ActionSheet cancelLabel, TabBar label and badgeLabel, AppBar backLabel.

Known limits

  • With reduced motion, a press on a sheet's handle in the first moments after it opens closes it instead of resizing it (UI SDK's Drawer; reported upstream).
  • TimeWheel is a 12- or 24-hour clock in its own words; it doesn't read the page's locale.