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/mobileParts
| Part | What it is |
|---|---|
SwipeList | Rows that swipe aside to actions on either side; a long swipe runs the first (createSwipeActions) |
PullToRefresh | Pull down at the top to refresh, with resistance (rubberBand); a Refresh button for the keyboard |
WheelPicker | A wheel you scroll to choose from (bindScrollPicker on a Listbox) |
TimeWheel | Hour, minute and AM/PM wheels, as "HH:MM" |
Sheet | A bottom sheet, as tall as its content or resting at snap points (Drawer) |
ActionSheet | A sheet of choices with Cancel; await sheet.choose() returns the one chosen |
AppBar, TabBar | The top bar (back, title, actions) and the bottom bar (sections, the current one marked) |
Use
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>
startactions show by swiping toward the end (left to right in English),endactions the other way; it flips in right-to-left pages. A side's first action sits at the row's edge. PastfullSwipe(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
labelis named by its content's text. PullToRefreshwaits for itsonRefreshPromise. A rejected one is announced ("Couldn't refresh.") and handed toonError. 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
Sheetwith 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. WheelPickerkeeps 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.TimeWheelshows a value between itsminuteSteps too (07 with a step of 5).- A
TabBaritem withhrefis a link, and a click with a modifier key is left to the browser (a new tab); withouthrefit's a UI SDK Button. Badges are a number on screen and words in the name ("Inbox, 2 new"). AppBartakesheadingLevel(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
| Part | Keys |
|---|---|
| SwipeList | Up 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 |
| PullToRefresh | Tab reaches a Refresh button at the top of the list |
| WheelPicker | Arrows, Home, End, Page Up and Down, and typing, like any Listbox |
| Sheet | Escape 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, likerun("Ada Lovelace", "Archive");openRow(row)opens one. Agents read the rows withquery, each with its actions. Adangeraction isn't run this way: the step fails with "Needs a person" and points at the row's Actions menu, whose item carriesdata-agent-confirm, so pressing it there asks the person. ActionSheetchoices markeddestructivecarrydata-agent-confirmtoo.- Tab bar sections, app bar buttons, wheels (
setValueon "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).
TimeWheelis a 12- or 24-hour clock in its own words; it doesn't read the page's locale.