Android: the app feels like a web app — kill tap highlights, add native-feeling motion #54

Closed
opened 2026-08-18 05:58:15 +00:00 by logan · 2 comments
Collaborator

Report

A lot of the app makes it obvious it is a web view. Elements get a highlight box when tapped, showing their bounding box. There are also opportunities for small, smooth animations and press states that would make it feel native.

Findings

  • Chrome 113 on the reference device: -webkit-tap-highlight-color, touch-action and overscroll-behavior are all available. user-select on interactive surfaces is worth setting too — a long press currently risks starting a text selection alongside our own long-press gesture (utils/long-press.ts, 500ms, dispatching a synthetic contextmenu).
  • Motion must respect prefers-reduced-motion; the app already has a rule about it outranking app settings (see now-playing's marquee).

Direction

A pass with a shared stylesheet: transparent tap highlight, user-select: none on rows/cards/controls, touch-action: manipulation (also removes the 300ms delay), overscroll-behavior: contain on scrollers. Then press states (a quick scale/opacity on active), view transitions between screens, and a sheet-style presentation for menus (see the bottom-sheet context menu issue). Check each against reduced-motion and against Chrome 113's feature set.

**Report** A lot of the app makes it obvious it is a web view. Elements get a highlight box when tapped, showing their bounding box. There are also opportunities for small, smooth animations and press states that would make it feel native. **Findings** - Chrome 113 on the reference device: `-webkit-tap-highlight-color`, `touch-action` and `overscroll-behavior` are all available. `user-select` on interactive surfaces is worth setting too — a long press currently risks starting a text selection alongside our own long-press gesture (`utils/long-press.ts`, 500ms, dispatching a synthetic `contextmenu`). - Motion must respect `prefers-reduced-motion`; the app already has a rule about it outranking app settings (see `now-playing`'s marquee). **Direction** A pass with a shared stylesheet: transparent tap highlight, `user-select: none` on rows/cards/controls, `touch-action: manipulation` (also removes the 300ms delay), `overscroll-behavior: contain` on scrollers. Then press states (a quick scale/opacity on active), view transitions between screens, and a sheet-style presentation for menus (see the bottom-sheet context menu issue). Check each against reduced-motion and against Chrome 113's feature set.
logan added the Area/DesignKind/EnhancementPlatform/Android
Priority
Medium
3
labels 2026-08-18 14:36:13 +00:00
logan self-assigned this 2026-08-24 07:36:57 +00:00
logan added the
Status
In Progress
label 2026-08-24 07:36:57 +00:00
Author
Collaborator

Picking this up on feat/54-native-touch-feel, from origin/main.

Approach, and the scope it draws, because this issue is a pass and a
pass is where scope goes wrong:

  1. The tap highlight goes globally, in one declaration.
    -webkit-tap-highlight-color is an inherited property, and an
    inherited property crosses a shadow boundary — so html { … : transparent } in index.css reaches every one of the app's shadow
    roots without a per-component rule. Today exactly one component sets
    it (library-status-indicator), which is why the bounding box shows
    everywhere else.

  2. Removing the highlight removes the only touch feedback some
    surfaces have
    , so a press state ships in the same change or the
    app goes from "wrong feedback" to "none". The cards already have
    one (transform: scale(0.97) on :active, in six components); the
    rows and the phone's menu items do not. That is where the press
    state is added, matching the existing idiom rather than inventing a
    second one.

  3. user-select is already done. index.css's first rule is
    *, *::before, *::after { user-select: none }, which — same
    inheritance argument — already covers the shadow roots. Nothing to
    do; recording it so the next reader does not re-derive it.

Deliberately not in this pass, both with reasons rather than as an
omission:

  • touch-action: manipulation. The Findings offer it as "also
    removes the 300ms delay", and this app's viewport is already
    width=device-width, which is what removes that delay in Chrome —
    so the stated benefit is not available to be won here. What is
    available is a change to the gesture stack that #63 tuned by
    measurement on the device
    (pan-y on the swipe surfaces plus a
    non-passive preventDefault), and I cannot measure on the device.
    An unmeasured global touch-action over a measured gesture model is
    the wrong trade.
  • View transitions between screens. That is a shell change
    (index.ts keeps every primary view mounted and toggles a class),
    it interacts with the history stack, and it is a design decision
    rather than a defect. If it is still wanted after this pass I will
    file it as its own issue with the shell argument in it.
  • The sheet-style menu presentation the Direction mentions is #60,
    which has already shipped.

One sequencing note: this issue is Phase 4's "polish pass over the same
surfaces" as #63/#67/#71, and #67 and #71 are open PRs touching the
lists and the nav sheet right now. Everything above is either one rule
in index.css or a :active block in a row's own stylesheet, so it
does not rewrite anything those PRs are moving.

Picking this up on `feat/54-native-touch-feel`, from `origin/main`. **Approach**, and the scope it draws, because this issue is a pass and a pass is where scope goes wrong: 1. **The tap highlight goes globally, in one declaration.** `-webkit-tap-highlight-color` is an *inherited* property, and an inherited property crosses a shadow boundary — so `html { … : transparent }` in `index.css` reaches every one of the app's shadow roots without a per-component rule. Today exactly one component sets it (`library-status-indicator`), which is why the bounding box shows everywhere else. 2. **Removing the highlight removes the only touch feedback some surfaces have**, so a press state ships in the same change or the app goes from "wrong feedback" to "none". The cards already have one (`transform: scale(0.97)` on `:active`, in six components); the *rows* and the phone's menu items do not. That is where the press state is added, matching the existing idiom rather than inventing a second one. 3. **`user-select` is already done.** `index.css`'s first rule is `*, *::before, *::after { user-select: none }`, which — same inheritance argument — already covers the shadow roots. Nothing to do; recording it so the next reader does not re-derive it. **Deliberately not in this pass**, both with reasons rather than as an omission: - **`touch-action: manipulation`.** The Findings offer it as "also removes the 300ms delay", and this app's viewport is already `width=device-width`, which is what removes that delay in Chrome — so the stated benefit is not available to be won here. What is available is a change to the gesture stack that #63 tuned *by measurement on the device* (`pan-y` on the swipe surfaces plus a non-passive `preventDefault`), and I cannot measure on the device. An unmeasured global `touch-action` over a measured gesture model is the wrong trade. - **View transitions between screens.** That is a shell change (`index.ts` keeps every primary view mounted and toggles a class), it interacts with the history stack, and it is a design decision rather than a defect. If it is still wanted after this pass I will file it as its own issue with the shell argument in it. - **The sheet-style menu presentation** the Direction mentions is #60, which has already shipped. One sequencing note: this issue is Phase 4's "polish pass over the same surfaces" as #63/#67/#71, and #67 and #71 are open PRs touching the lists and the nav sheet right now. Everything above is either one rule in `index.css` or a `:active` block in a row's own stylesheet, so it does not rewrite anything those PRs are moving.
Author
Collaborator

PR #214#214. CI
green on both jobs (chromium 241, WebKit 239 + the 2 pre-existing
swipe skips).

What landed, against what this issue asked for:

  • The tap highlight is gone, in one inherited declaration on
    html — measured rgba(0, 0, 0, 0.18) before, rgba(0, 0, 0, 0)
    after, read three shadow roots deep.
  • Press states on the surfaces that had none: the four lists'
    rows, the tab bar, the sidebar's destinations and the shared
    context-menu item. The cards already had scale(0.97). This is part
    of removing the highlight rather than an extra, because with the
    highlight gone and no press rule a held row measures the hover
    tint — which on a phone is synthesised by the hold and outlives it.
    Those hover tints moved behind (hover: hover) and (pointer: fine)
    in the same pass, which is #68's gate applied to a tint.
  • user-select needed nothing: index.css's first rule already
    covers the shadow roots by the same inheritance.
  • touch-action: manipulation declined, and overscroll-behavior
    too — reasons on the PR; both come down to the stated benefit not
    being available here (width=device-width already removes the tap
    delay; body cannot scroll, so there is no chaining to contain) set
    against changing a gesture stack tuned on a device this session
    cannot measure.
  • Motion and view transitions — the other half of the title — are
    #213. That half is a device judgement (this codebase already
    carries a measured "transitions removed, software rendering repaints
    per frame" on two of its grids) and, for view transitions, a shell
    change that interacts with the history stack. Splitting it is what
    keeps this PR's claims checkable.

Leaving Status/In Progress on until the PR merges.

**PR #214** — https://git.ljones.me/yonlu/yellowjacket/pulls/214. CI green on both jobs (chromium 241, WebKit 239 + the 2 pre-existing swipe skips). What landed, against what this issue asked for: - **The tap highlight** is gone, in one inherited declaration on `html` — measured `rgba(0, 0, 0, 0.18)` before, `rgba(0, 0, 0, 0)` after, read three shadow roots deep. - **Press states** on the surfaces that had none: the four lists' rows, the tab bar, the sidebar's destinations and the shared context-menu item. The cards already had `scale(0.97)`. This is part of removing the highlight rather than an extra, because with the highlight gone and no press rule a held row measures the *hover* tint — which on a phone is synthesised by the hold and outlives it. Those hover tints moved behind `(hover: hover) and (pointer: fine)` in the same pass, which is #68's gate applied to a tint. - **`user-select`** needed nothing: `index.css`'s first rule already covers the shadow roots by the same inheritance. - **`touch-action: manipulation`** declined, and **`overscroll-behavior`** too — reasons on the PR; both come down to the stated benefit not being available here (`width=device-width` already removes the tap delay; `body` cannot scroll, so there is no chaining to contain) set against changing a gesture stack tuned on a device this session cannot measure. - **Motion and view transitions** — the other half of the title — are **#213**. That half is a device judgement (this codebase already carries a measured "transitions removed, software rendering repaints per frame" on two of its grids) and, for view transitions, a shell change that interacts with the history stack. Splitting it is what keeps this PR's claims checkable. Leaving `Status/In Progress` on until the PR merges.
logan closed this issue 2026-08-25 17:48:45 +00:00
gitea-actions bot removed the
Status
In Progress
label 2026-08-25 17:51:24 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#54