Skip to content

Touch mode

Components present and behave for how they are operated: a popover under a mouse, a bottom sheet under a thumb.

Youj does not shrink a desktop interface to fit a phone. A component changes how it is presented and operated depending on the input.

  • Under a pointer, a picker is a popover that grows out of its trigger.
  • Under a thumb, the same picker is a bottom sheet with a grab handle and 48px rows. Controls are 44px or taller (a small control keeps its look and takes an invisible 44px hit area), text fields are 16px so iOS does not zoom, and nothing depends on hover.

This is separate from responsive layout. A layout follows the width of the window. Touch mode follows how the page is operated, so a wide tablet is touch and a narrow desktop window is not.

What changes on touch

ComponentPointerTouch
Dropdown Menu, Context MenuPopover; right-clickAction sheet; long press
SelectPopover over the triggerBottom sheet
Combobox, Multi-selectPopover with a search fieldSheet with the search field on top, above the keyboard
Command, Command PaletteCentered dialogTall sheet
Popover, Date Picker, Date Range Picker, Color PickerPopoverSheet; 44px day cells and swatches
DialogCentered windowBottom sheet that is pulled down to dismiss, above the keyboard
Alert DialogCentered windowStays centered and is not draggable; actions stack at 44px
SheetPanel on an edgeFull width up to 28rem, with a 44px close button
TooltipHover and focusDoes not open by default; opt in to a long press with longPress
Hover CardHover and focusA tap opens it; a second tap follows the link
Controls (Button, Input, Tabs, Toggle)24 to 40px8px taller, default 44px, small sizes with a 44px hit area; inputs at 16px
Keyboard shortcut hintsShownHidden

Each component's page has a Touch section with the details. Sheet Presentation covers building your own.

Setting the device

The default is auto: the primary pointer decides ((pointer: coarse) is touch). Force one for a subtree, and for everything it portals, with device on YoujProvider or ThemeScope.

import { YoujProvider, ThemeScope } from '@youj/core'

<YoujProvider>
  {/* follows the hardware */}
  <ThemeScope device="touch">
    <Page /> {/* sheets and touch sizes, even on a desktop */}
  </ThemeScope>
</YoujProvider>

Set it once, near the root of what it governs. A scope nested inside a forced one can force the other way for its own parts, but it cannot hand the choice back to auto.

portalContainer on the provider sends the overlays of a subtree to an element you choose instead of the end of <body>. The docs use it to show a phone's sheets inside a phone-sized frame on a desktop (see below).

In your own components

import { useDevice, useDeviceSetting, usePortalContainer } from '@youj/core'

const device = useDevice() // 'touch' | 'pointer'
const setting = useDeviceSetting() // 'auto' | 'touch' | 'pointer'

useDevice says what a component should do when it opens. The server and the hydration render say pointer and the client corrects on mount, so use it for behavior at open time, not for markup that is on the page at load. For styling use the touch: and fine: variants, which follow the hardware or a forced device (use these rather than Tailwind's pointer-coarse:, which cannot see a forced one).

@youj/core also exports the building blocks the provider uses: DEVICE_SETTINGS, DEFAULT_DEVICE_SETTING, isDeviceSetting and resolveDevice(setting, coarse).

Previewing touch on a desktop

Every demo in these docs has a Pointer and Touch control. On a desktop, Touch shows the demo in a phone-sized screen, where menus, selects and dialogs rise as sheets from the bottom of that screen, and a mouse drag works as a finger would. The choice is remembered, and it defaults to your hardware. In Storybook, the toolbar's Device control does the same for every story.

What is not done

  • A sheet has one height. Half and full detents are not built.
  • A submenu opens as another sheet above its parent. Native menus push inside one sheet.
  • There is no haptic feedback. The web has no equivalent on iOS.
  • On an iPad, native menus are popovers. Pass presentation="popover" to keep one.
  • Real keyboards cannot be emulated, so the keyboard handling is verified against a simulated visual viewport.