w3c

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars7
mentions62
036912Oct 4Oct 18Nov 2
UTC days · repositories w3c owns3 repositories

Package downloads

No package download history available yet.

Top repositories

w3c’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside w3c’s account where w3c opened or reviewed pull requests in the past 365 days.

Who noticed w3c

Everyone who starred or forked w3c’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

69posts
Nov 37 posts
  1. Web Vibration API: Haptic Feedback on Android and iPhone TL;DR As of October 2026 the Web Vibration API (navigator.vibrate) works only in Chromium browsers on Android, only after the user has tapped the page, and with no control over intensity; Safari on iPhone has never shipped it. The one haptic a page can trigger on an iPhone is the system tick of a native HTML switch, and since iOS 26.5 WebKit plays it only when the user's own tap toggles the switch, never for a click from JavaScript. Put navigator.vibrate behind one helper that returns false instead of throwing, lay a label for a hidden switch over buttons that should tick on iPhone, and never let a tick be the only feedback. You want a tap to tick under the thumb the way it does in a native app, so you reach for the web vibration API. On an Android phone navigator.vibrate(10) works on the first try. On an iPhone nothing happens, on a laptop the call returns true and nothing happens, and the iPhone workaround you found on GitHub stopped working in iOS 26.5. This tutorial maps what a page can really trigger in October 2026, builds a helper that stays silent wherever it cannot play, and wires haptics into a switch, a button and a wheel picker in React. What can the Web Vibration API do in 2026? It can switch the vibration motor on and off for durations you give in milliseconds, in Chromium browsers on Android, after the user has tapped the page. That is the whole API: one method, navigator.vibrate, that takes a number or an array of alternating vibrate and pause times. There is no intensity, no waveform and no way to ask whether the device has a motor at all. Software Mansion's August 2026 overview of web haptics sums up the support in one line: "the vibration API is effectively limited to Chromium-based browsers." Here is where a tick can reach a finger, as of October 2026 (caniuse for support, our own test in desktop Chromium for the last row): | Where the page runs | navigator.vibrate | What the user feels | | --- | --- | --- | | Chrome or Samsung Internet on Android | supported | the pattern you pass, once the page has seen a tap | | Firefox | removed in version 129, not supported on Android | nothing | | Safari on iPhone, and home screen web apps | never shipped | the system tick of a native switch, only when the user's own tap toggles it | | Chrome on a desktop | present, returns true | nothing, there is no motor | The last row is the trap. Feature detection with "vibrate" in navigator says yes in desktop Chrome, so it tells you nothing about hardware. The boolean that navigator.vibrate() returns only says the browser accepted the pattern; it says nothing about whether anyone felt it. How does navigator.vibrate behave on Android? It plays the pattern you pass and replaces any pattern already running, within limits you can read in Chromium's vibration controller: navigator.vibrate(8); // one 8ms pulse: a light tick navigator.vibrate([10, 60, 16]); // pulse, pause, pulse: a success pattern navigator.vibrate(0); // cancels whatever is running (so does []) - It needs a tap first. MDN lists sticky user activation as a requirement. Until the page has seen a tap, a click or a key press, Chrome returns false and logs "Blocked call to navigator.vibrate because user hasn't tapped on the frame or any embedded frame yet". Once granted, the activation lasts until the page navigates, so a tick from a scroll handler or after an await works later on. - It is silent in the background. The call returns false while the page is hidden. - Long patterns are trimmed. Chromium caps each entry at 10,000ms, keeps the first 99 entries and drops a trailing pause. - The phone can overrule you. MDN notes that some devices do not vibrate in silent or Do Not Disturb mode. Keep the pulses short; the helper below uses 4 to 22ms per pulse. The 200ms buzz in many examples (MDN's first one included) reads as a notification, which is the wrong message for a toggle. Does the Web Vibration API work on iOS? No. Safari has never implemented navigator.vibrate, and caniuse lists no version of Safari on iOS with it, up to 27.2. Look up web vibration API iOS support and you land on one workaround: Safari 17.4 added a native switch control, <input type="checkbox" switch> (WebKit's announcement), and toggling it plays the system haptic tick. Until iOS 26.5, libraries such as ios-haptics played that tick on demand by calling .click() on a hidden label wired to the switch. That no longer works. The ios-haptics README of April 2026 says the trick "only works on ios 17.4 to 26.4, as apple patched it in ios 26.5". The WebKit fix, merged into the main branch on May 21, 2026, closes that gap: WebKit already required user activation for the haptic, and now it also requires a trusted event, which a click forwarded from label.click() no longer is. What still works is a real tap that lands on a label for the switch. The tap produces a trusted click, the label forwards it to the switch, and because the original click was trusted, the WebKit change still lets the switch tick. The ios-haptics source moved to exactly that on September 22, 2026: it lays a transparent label over your element. Here is the same idea as a React 19 component: // components/ios-tap-tick.tsx "use client"; // iOS plays the switch haptic only for a trusted click, so the user's own tap has to land on a label for the switch. // put it inside a positioned button; it covers the button and the click still bubbles to the button's onClick export function IOSTapTick() { return ( <label aria-hidden="true" data-slot="ios-tap-tick" className="absolute inset-0 touch-manipulation [-webkit-tap-highlight-color:transparent]" > <input type="checkbox" // react 19 passes the unknown attribute through; the spread keeps TypeScript quiet {...{ switch: "" }} tabIndex={-1} // the label, not the switch, sits under the finger, so a swipe that starts here still scrolls the page className="invisible absolute m-0 size-px" // the label forwards the click to the switch: stop that second click before it reaches the button onClick={(event) => event.stopPropagation()} /> </label> ); } Use it in buttons, not links: in our Chromium test a label inside an <a> ran the link's onClick but took over the default action, so the link never navigated. Leave the input without a name, so it never ends up in a form submission. Outside iOS a tap on it only toggles a hidden checkbox nobody sees, so it can render on every platform and never causes a hydration mismatch. On an iPhone you get one fixed tick per tap, from the tap itself: no patterns, nothing after an await, nothing during a drag, and nothing for a wheel spinning past rows, because a scroll never produces a click. How do you call navigator.vibrate safely where it is unsupported? Check for the method itself, check activation, catch everything, and return a boolean. Here is the whole helper without dependencies: // lib/vibrate.ts export type HapticKind = "selection" | "light" | "medium" | "heavy" | "success" | "warning" | "error"; const PATTERNS: Record<HapticKind, number | number[]> = { selection: 4, light: 8, medium: 14, heavy: 22, success: [10, 60, 16], warning: [16, 70, 16], error: [22, 50, 22, 50, 22], }; let enabled = true; export function setHapticsEnabled(value: boolean) { enabled = value; } export function vibrate(kind: HapticKind = "light"): boolean { if (!enabled) return false; // node 21+ has a global navigator too, so test for the method, not for navigator if (typeof navigator === "undefined" || typeof navigator.vibrate !== "function") return false; // chrome blocks the call (and logs a warning) until the page has seen a tap if (navigator.userActivation && !navigator.userActivation.hasBeenActive) return false; try { return navigator.vibrate(PATTERNS[kind]); } catch { return false; // feedback is never worth an exception } } The Node check matters. Node 21 and later define a global navigator with no vibrate, so a helper that only checks typeof navigator gets past its guard on the server and calls a method that is not there. Wingo UI's haptics helper is the same idea plus an iPhone fallback and a force option that plays a tick even when haptics are off. haptic(kind) returns whether a haptic was sent, configureHaptics({ enabled, iosFallback }) sets the app-wide options, and supportsHaptics() tells you whether the browser exposes a haptic path at all (call it in an effect: it is false on the server, and true in desktop Chrome for the reason above). > Live demo: Haptics. The original article embeds an interactive demo here (On an Android phone, tap each tile to feel the seven kinds; the bars draw each vibration pattern and the line under the card shows what haptic() returned). Try it live on wingo-ui.com or open the Haptics docs. Install: npx wingo-ui@latest add haptics A caveat about our own code: the helper's iosFallback option, on by default, still clicks a hidden label from script. That plays on iPhones up to iOS 26.4, is ignored from 26.5, and the call still returns true there because WebKit reports nothing back. If you add the tap overlay above, call configureHaptics({ iosFallback: false }) once so iPhones on older versions do not tick twice. The snippets below import Wingo UI's helper. With the dependency-free file above, import vibrate from @/lib/vibrate instead and use setHapticsEnabled in place of configureHaptics; it has no force option. Which haptic should each interaction get? One tick per event, sized to the event. These are the patterns the helper sends on Android: | Kind | Pattern (ms) | Use it for | Wingo UI component that plays it | | --- | --- | --- | --- | | selection | 4 | a row of a wheel or a slider step passing the center | Picker Wheel | | light | 8 | a tap, a toggle | Haptic Button press | | medium | 14 | a long press, a snap, a crossed threshold | Haptic Button long press, Pull To Refresh arming, Slide To Confirm armed | | heavy | 22 | a committed full swipe, a drop | none; call it from your own drop handler | | success | 10, 60, 16 | payment sent, item saved | Haptic Button hold confirmed, Slide To Confirm done | | warning | 16, 70, 16 | a limit reached, a hold released too early | Haptic Button tap in hold mode | | error | 22, 50, 22, 50, 22 | a wrong PIN, a failed save | Haptic Button rejected promise | Three rules keep it from turning into noise. Call it from the handler of a real gesture, never from an effect or a render. Never tick per animation frame; tick when something changes state, like a row reaching the center. And never put information only in the tick: desktop users, iPhone users on a wheel and anyone with a phone on silent will not feel it, so the screen must say the same thing. How do you add React haptic feedback to a switch, a button and a wheel? Call haptic() in the handler where the state changes, once per change. A switch The free Switch calls onCheckedChange on every click, key or drag, and a returned promise makes it spin, then flip back with errorText if the save fails. Tick on the change and again on failure: "use client"; import { Switch } from "@/components/ui/switch"; import { haptic } from "@/lib/haptics"; export function NewMessagesSetting({ save }: { save: (on: boolean) => Promise<void> }) { return ( <Switch label="New messages" description="When someone writes to you about a listing" labelPosition="start" defaultChecked onCheckedChange={async (on) => { haptic("light"); try { await save(on); } catch (error) { haptic("error"); throw error; // the switch flips back and shows errorText } }} /> ); } On iOS 26.5 and later this Switch stays silent: it is a Radix button with role="switch", not the native input, so no system tick comes with the tap, and haptic() cannot reach the switch tick from script. If an iPhone tick on a settings toggle matters more to you than the custom look, a plain <input type="checkbox" switch> ticks on its own and renders as a checkbox in browsers that do not know the attribute. A button The Haptic Button plays its hapticKind (default light) on touch and pen presses only, so a mouse click never asks for a vibration. In mode="hold" it plays light when the hold starts, success once onConfirm has finished, warning for a tap that was too short and error when onConfirm rejects; with onLongPress set, a long press plays medium. "use client"; import { Send } from "lucide-react"; import { HapticButton } from "@/components/ui/haptic-button"; export function SendRent({ onSend }: { onSend: () => Promise<void> }) { return ( <HapticButton mode="hold" fullWidth leftIcon={<Send />} labels={{ holdHint: "Hold to send" }} onConfirm={onSend}> Hold to send $1,850 </HapticButton> ); } > Live demo: Haptic Button. The original article embeds an interactive demo here (Hold the Hold to send button with a finger on an Android phone: a light tick starts the fill, a success pattern lands with the check, and a quick tap shakes the button with a warning tick). Try it live on wingo-ui.com or open the Haptic Button docs. Install: npx wingo-ui@latest add haptic-button For an ordinary Button that should tick on both platforms, combine the call and the overlay. The Button is already position: relative, so the overlay covers it: "use client"; import { Button } from "@/components/ui/button"; import { IOSTapTick } from "@/components/ios-tap-tick"; import { haptic } from "@/lib/haptics"; export function SaveButton({ onSave }: { onSave: () => Promise<void> }) { return ( <Button onClick={async () => { haptic("light"); // android; the overlay ticks on iPhone try { await onSave(); haptic("success"); // android only: iPhone has no tick after an await } catch { haptic("error"); } }} > Save <IOSTapTick /> </Button> ); } A wheel The Picker Wheel plays a selection tick each time a new row reaches the center while a finger, a mouse drag or a scroll wheel drives it, and never for keys, clicks or value changes from outside. haptics={false} turns it off. "use client"; import { PickerWheel, pickerRange } from "@/components/ui/picker-wheel"; const hours = pickerRange(8, 19, { pad: 2 }); const minutes = pickerRange(0, 55, { step: 5, pad: 2 }); export function AppointmentTime() { return ( <PickerWheel aria-label="Appointment time" loop defaultValue={["10", "30"]} columns={[ { id: "hour", label: "h", options: hours }, { id: "minute", label: "min", options: minutes }, ]} /> ); } > Live demo: Picker Wheel. The original article embeds an interactive demo here (Flick a column on an Android phone: each row that reaches the center ticks once, and the value changes when the wheel comes to rest). Try it live on wingo-ui.com or open the Picker Wheel docs. Install: npx wingo-ui@latest add picker-wheel This is the clearest case of the platform gap. On Android every row that passes the center ticks. On an iPhone the wheel is silent from iOS 26.5, because a spin is a scroll and never produces the trusted click the switch needs; the band, the tilt and the snap carry the feedback there. How do you let people turn haptics off? Give them a setting and pass it to the helper once. Every haptic feedback web app needs one: some people find vibration distracting, and the browser gives a page no way to read the phone's own haptics setting. "use client"; import { useEffect, useState } from "react"; import { Switch } from "@/components/ui/switch"; import { configureHaptics, haptic } from "@/lib/haptics"; export function VibrationSetting() { const [on, setOn] = useState(true); useEffect(() => { configureHaptics({ enabled: on }); }, [on]); return ( <Switch label="Vibrations" description="A short tick when you tap, hold or pick" labelPosition="start" checked={on} onCheckedChange={(next) => { setOn(next); // the effect has not run yet, so force the confirmation tick if (next) haptic("light", { force: true }); }} /> ); } Store the value with the rest of the user's settings. Every component that ticks through the helper follows it without a prop: the Haptic Button, the Picker Wheel, and the Pull To Refresh from our pull to refresh guide among them. What comes after navigator.vibrate? A semantic API with named effects, if browsers adopt the one being drafted now. The Web Haptics API started as a proposal to the CSS Working Group in March 2026 and has been incubated at the WICG since April. Its draft has a navigator.playHaptics(effect, intensity) call and a nested @haptic CSS rule, with six named effects (hint, edge, tick, align, success, error) and an intensity from 0 to 1 that the browser maps to the platform's own haptics. The call returns undefined, so a page learns nothing about the device. As of October 2026 Chrome Platform Status lists Web haptics as Proposed, and no stable browser ships it. The shape is familiar: named kinds instead of millisecond patterns. If your app already funnels every tick through one haptic(kind) call, moving to a new API later means changing one file. Where should you start? Add a helper first, then tick the three places users touch most: toggles, the primary button and any wheel or slider. The dependency-free lib/vibrate.ts above covers Android. npx wingo-ui@latest add haptics copies Wingo UI's lib/haptics.ts instead; it is part of Wingo UI Pro, like the Haptic Button and the Picker Wheel, while the Switch is free. For the rest of the phone checklist (bottom sheets, 44px touch targets, safe area insets), read mobile-first React components or browse the Mobile UI in React category. Components in this post - Haptics (Pro): Haptic feedback for the web in one call: short vibration ticks on Android and the system tick on iPhone, silent everywhere else. Install: npx wingo-ui@latest add haptics - Haptic Button (Pro): A button that answers like a native control: spring press, touch ripple, a haptic tick, a long-press action and a hold-to-confirm mode. Install: npx wingo-ui@latest add haptic-button - Switch (Free): An on / off setting that applies at once: a spring thumb that stretches and drags like iOS, thumb icons, and async saves that roll back. Install: npx wingo-ui@latest add switch - Picker Wheel (Pro): The iOS wheel picker for the web: columns that spin with native momentum and click into place on a 3D cylinder, inline or as a field. Install: npx wingo-ui@latest add picker-wheel FAQ Does navigator.vibrate work on iPhone? No. Safari has never implemented the Vibration API, and as of October 2026 caniuse lists no version of Safari on iOS that supports it. The only haptic a web page can produce on an iPhone is the system tick of a native switch input, and since iOS 26.5 only the user's own tap on it plays that tick. Why does navigator.vibrate return true on my laptop? Because true only means the browser accepted the pattern. Desktop Chrome exposes navigator.vibrate and, once the page has seen a click, returns true for a valid pattern even though there is no motor to run. Use the return value to detect refusals and never to detect hardware. Why does navigator.vibrate do nothing on my Android phone? The usual causes are a page the user has not tapped yet (Chrome blocks the call and logs a warning until the page has sticky user activation), a hidden tab, silent or Do Not Disturb mode, or Firefox, which removed the API in version 129. Can a web page control vibration intensity? Not with the Vibration API, which only switches the motor on and off for the durations you pass. The Web Haptics API proposal, incubated at the WICG since April 2026, adds named effects with an intensity from 0 to 1, but as of October 2026 no stable browser ships it. Is the Wingo UI haptics helper free? No. The haptics helper, the Haptic Button and the Picker Wheel are Pro items; the Switch is free. With a Pro account signed in through npx wingo-ui@latest login, npx wingo-ui@latest add haptics copies lib/haptics.ts into your project. --- This article was originally published on the Wingo UI blog: Web Vibration API: Haptic Feedback on Android and iPhone. The original has the live, interactive demos and is kept up to date.

  2. EPUB航海日誌 — 3.4の海図に合わせるだけの、短い航海のはずだった EPUB 3.4 が来年出るらしい。 電子書籍の形式である EPUB の、次の版である。 W3C の作業部会の予定表には、こう書いてある。 2027 February: EPUB 3.4 final version こちらは AozoraEpub3-JDK21 という、青空文庫のテキストや Web 小説を電子書籍にする道具を保守している。 今の EPUB 3.3 に合っているか見直し、次の 3.4 の草案も読んでおこう。 古いところを直せば終わりである。 サクッと済ませよう。 出港前に、予定の航路を書いておいた。 予定の航路 1. 仕様を読む 2. 古い要素を消す 3. 検証器が通ることを確かめる 4. 出す 4 行である。 短い航海の予定だった。 以前、RAW ファイルから表示用の画像を取り出す話を書いた。あのときは、カメラが作った実物に何度も予想を外された。 今回は国際標準がある。しかも原文まで読める。 今度こそ、まっすぐ進めるはずである。 本稿は、その航海の日誌である。 EPUB を知らなくても読めるように書く。途中で仕様の話が出るが、読めなくても船は進む。 先に結論の一部だけ言っておくと、予定の 4 行のうち、2 行目と 3 行目は出港した日に消えた。 出港前: 積荷を見ておく ■ 出港前 天候: 晴れ 位置: EPUB 3.4 が来年出ると知った 積荷: 縦書きの小説 予定: 短い航海 航海に出る前に、積荷を見ておく。 今回の積荷は、縦書きの小説である。 例 1: 完成品 AozoraEpub3-JDK21 で、宮沢賢治の『銀河鉄道の夜』を電子書籍にした。 青空文庫で公開されている作品である。 Apple Books で開くと、こうなる。 Apple Books(macOS)。縦書き、見出し、ルビ 縦書きである。 「言」の横に小さく「い」、「黒板」の横に「こくばん」が付いている。 ルビ(ふりがな)である。 紙の文庫本で見慣れた組み方が、画面に出ている。 文字を大きくしたり、読む端末を変えたりしても、本として読める。 これが EPUB の面白いところである。 例 2: 船の中身 では EPUB とは何か。 正体は、決まったルールで中身を詰めた ZIP ファイルである。 多くの環境では、拡張子を .zip に変えれば中身を開いて見られる。 『銀河鉄道の夜』の中身は、こうなっていた。 mimetype META-INF/container.xml OPS/package.opf ← 積荷目録 OPS/xhtml/nav.xhtml ← 目次 OPS/xhtml/title.xhtml ← 表題ページ OPS/xhtml/0001.xhtml ← 本文 OPS/xhtml/0002.xhtml OPS/css/vertical.css ← 縦書きの見た目 OPS/css/vertical_font.css OPS/css/vertical_text.css OPS/css/vertical_middle.css OPS/css/vertical_image.css .xhtml はウェブページである。ただし、正確には XHTML という、XML のルールに従って書く HTML である。 XML。 SOAP……うっ、頭痛が……。 昔の仕事で苦労した記憶が少しよみがえった。話を戻そう。 .css はウェブページの見た目の指定である。 つまり、 EPUB は、ざっくり言えばウェブページを ZIP に詰めた本である。 もちろん ZIP の中での置き方にも決まりはある。 本文はウェブページで書く。 見た目は CSS で決める。 何が入っているかは、package.opf という積荷目録に書く。 ほぼ、それだけである。 例 3: 変換の矢印 AozoraEpub3-JDK21 がやっていることは、この矢印である。 青空文庫のテキストには、ルビや見出しが記号で書いてある。 [#5字下げ]一 午後の授業[#「一 午後の授業」は中見出し] 「ではみなさんは、そういうふうに川だと言《い》われたり、乳《ちち》の流《なが》れたあとだと…… 言《い》 が「言」にルビ「い」である。 これを、EPUB の中ではウェブページの書き方にする。 <div class="chap2">一<span class="fullsp"> </span>午後の授業</div> <p>「ではみなさんは、そういうふうに川だと<ruby>言<rt>い</rt></ruby>われたり、<ruby>乳<rt>ちち</rt></ruby>の<ruby>流<rt>なが</rt></ruby>れたあとだと……</p> <ruby>言<rt>い</rt></ruby> が、例 1 の画面のルビになる。 例 4: 見た目は CSS が決めている 今度は同じ本文を横書きにしてみる。 同じ本文を横書きで 本文のウェブページは同じである。 変わったのは CSS だけである。 縦書き、ルビ、そして後で出てくる縦中横(「12」のような数字を、縦書きの中で 1 マスに横に並べる組み方)や傍点。 日本の小説に要るものは、どれも EPUB 3 では HTML と CSS の機能として書く。 EPUB 3 の概要文書にも、こうある。 EPUB 3 also supports CSS styles that enable both horizontal and vertical layout 縦書きとルビが EPUB に本格的に入ったのは、2011 年の EPUB 3.0 である。 それから十数年、3.0.1、3.1、3.2、3.3 と改訂が続いた。 3.3 からは、ウェブの標準を決めている W3C の勧告になった。 そして次が 3.4 である。 道具の素性も書いておく。 AozoraEpub3 は、2012 年に hmdev 氏が作り始めた道具である。青空文庫の注記を読んで、縦書きの EPUB を作る。 AozoraEpub3-JDK21 は、その派生版である。今の Java(Java 21)で動くようにし、Web 小説を電子書籍にする narou.rb から使えるように保守している。 本稿で「今の出力」と書くのは、AozoraEpub3-JDK21 の出力である。 ウェブページが作れるなら、電子書籍の中身も案外見慣れた世界にある。全く別の魔法ではない。 積荷の確認は、ここまで。 出港する。 出港の日: 海図を読む ■ 出港の日 天候: 晴れ/検証器は 0 件 位置: 3.3・3.4 の仕様を読み始めた 観測: 消すつもりだった要素は「消すべきもの」ではなかった 判断: 予定の航路の 2 と 3 が要らない 針路: 3.4 で何が変わるかを確かめる 予定の航路の 1 行目、仕様を読む。 計画書には、こう書いてあった。 古い 3 要素(表紙の meta・NCX・guide)を消して、警告を減らす。 EPUB 2 の時代の書き方が、今の出力に残っている。 それを消せば 3.3 対応、という算段である。 例 5: 積荷目録の実物 古い要素がどこにあるのか、実物を見ておく。 『銀河鉄道の夜』に表紙の画像を付けて、出港前の版(v1.7.6-jdk21)で変換した package.opf の抜粋である(「←」の注釈は本稿で足した)。 <metadata> <dc:title id="title">銀河鉄道の夜</dc:title> <dc:creator id="creator">宮沢賢治</dc:creator> <dc:language id="pub-lang">ja</dc:language> … <meta name="cover" content="img0000"/> ← 表紙の meta(EPUB 2 の書き方) </metadata> <manifest> <item id="nav" properties="nav" href="xhtml/nav.xhtml" media-type="application/xhtml+xml"/> <item id="sec0001" href="xhtml/0001.xhtml" media-type="application/xhtml+xml"/> … <item id="img0000" href="images/0000.png" media-type="image/png" properties="cover-image"/> <item href="toc.ncx" id="ncx" media-type="application/x-dtbncx+xml"/> ← NCX(EPUB 2 の目次) </manifest> <spine page-progression-direction="rtl" toc="ncx"> ← 右から左へめくる … </spine> <guide> ← EPUB 2 の書き方 … <reference type="text" title="sec0001" href="xhtml/0001.xhtml"/> </guide> <manifest> が、文字どおり積荷目録である。 表紙の meta、NCX、<guide>。計画書の「古い 3 要素」は、この 3 つである。 表紙は、EPUB 3 では properties="cover-image" の付いた行で示す。表紙の meta は、同じことを EPUB 2 の書き方で言っている。 <guide> は EPUB 2 の書き方で、「本文はここから」と港に教える行である。 これを消せばいい。 そう思って、EPUB 3.3 の原文を読んだ。 消す理由が無かった 3.3 の原文では、これらは deprecated(使うべきでない)ではなかった。 legacy だった。 「昔の EPUB 2 のリーダーのために残してある。入れてもよいが、EPUB 3 のリーダーは使わない」という扱いである。 しかも、EPUB Reading Systems 3.3 には、3.3 に準拠した本の legacy 要素をリーダーがサポートしてはならない、とある。 つまり作る側には「入れてもよい」、読む側には「使わないでほしい」と書かれている。 検証器も、legacy 要素があるだけでは警告しない。 念のため、手元の継続的なビルド(CI)が作っている EPUB を確かめた。 最初から 0 errors / 0 warnings だった。 減らす警告が、そもそも無い。 予定の航路の 2 行目「古い要素を消す」は、消す理由を失った。 3 行目「検証器が通ることを確かめる」は、最初から通っていた。 出港した日に、予定の 2 行が消えた。 3.4 の海図で見直したところ では、次の 3.4 はどうなのか。 3.4 の草案(2026 年 11 月 2 日版)の変更記録を読んだ。 3.4 で新しく整理されることもあれば、3.3 の時点ですでに決まっていたこともあった。 一つ目。3.4 では「outdated(古くなった機能)」という区分に整理される。 推奨はされない。でも今すぐ使えなくすると、世の中にある大量の EPUB が仕様に合わなくなる。そこで残される機能である。 さっきの legacy の要素も、この区分に含まれる。 二つ目。読む側には、以前から決まりがあった。 EPUB 3.3 の時点で「リーダーは legacy 要素を使わない」とされている。3.4 で初めて禁止されるわけではない。 だから仕様どおりなら、<guide> のような古い標識がなくても、新しい標識で本を開けるはずだった。 三つ目。縦書きの CSS にも、古い書き方が残っている。 ここは例を見たほうが早い。 例 6: 同じことを 4 回書いている CSS AozoraEpub3-JDK21 の縦中横の指定である(コメントは本稿で足した)。 .tcy span { text-combine-upright: all; /* 今の標準 */ text-combine: horizontal; /* 古い書き方 */ -webkit-text-combine: horizontal; /* ブラウザ用の古い書き方 */ -epub-text-combine: horizontal; /* EPUB 用の古い書き方 */ } 同じことを 4 回書いている。 リーダーによって、分かる書き方が違った時代の名残である。 縦書きも傍点も、同じように 3 回ずつ書いてある。 3.4 では、多くの -epub- で始まる書き方が outdated に整理される。 「CSS の対応が追いついたら、頭に何も付かない標準の書き方に移ってほしい」という方向である。 ただし最後の -epub-text-combine は別枠だ。これは 3.3 の時点ですでに deprecated だった。 四つ目。ルビの <rp> も確かめた。 <rp> は、ルビを理解しない画面のための保険である。 <ruby>漢字<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby> ルビを理解しない画面では「漢字(かんじ)」と括弧つきで出る。 ルビを理解する画面では、<rp> は表示されない。 3.3 の時点で、EPUB 3 のリーダーはルビを理解するので <rp> は使わないほうがよい、と勧めている。3.4 でもその考え方が続いている。 AozoraEpub3-JDK21 は、もともと <rp> を出していない。 Web 小説のページに <rp> 付きのルビがあれば、取り込むときに外している。 ここは新しい変更への対応というより、今の作りを確かめ直しただけだった。 ほかにも 3.4 では、絵の形式に AVIF と JPEG XL が加わったり、漫画を縦に流して読む形式が加わったりする。 ただ、縦書きの小説には直接は効かない。 ちなみに 3.4 の作業部会の名簿には、講談社、KADOKAWA、メディアドゥ、ボイジャー、楽天といった日本の会社が並んでいる。 海図を書いている人たちの中に、縦書きの国の人がいる。 方針 ここで方針を決めた。 今作った本が、将来も安心して残せる形に。そのうえで、できるだけ過去との互換を保つ。 既定は新しい形にする。 古い形は設定で戻せるようにする。 ここまでは海図の上の話である。 ところで、海図に「古い」と書いてある航路を、実際の港は使っていないのだろうか。 海図だけでは決められない。 港に寄って確かめることにした。 予定に無い寄港である。 海図は、まだ鉛筆で書かれている。 寄港: 4 つの港に寄る ■ 寄港 天候: 晴れ/検証器は 0 件 位置: 試作の本を 4 種類作った 観測: 1 つの港だけ、古い航路を使っていた 判断: 海図で古いと書かれた行でも、消せないものがある 針路: 目次も確かめる 寄った港は 4 つである。 Apple Books(macOS) Google Play ブックス(Android) Thorium(Windows。EPUB 専用のリーダー) Kindle Previewer(Windows。Kindle で見たときの見た目を確かめる道具) 試作の本を 4 種類作った。 A: 今の形 B: 新しい形(古い要素を外したもの) C: CSS から -epub- などの古い書き方を全部外したもの N: 対照 N は「比べれば差が見えるか」を確かめるための本である。 わざと崩しておいて、4 つの港で崩れが見えることを先に確かめた。 崩した本で差が見えなければ、A と B に差が無くても「差が無い」とは言えない。 計器が動いているかを、先に確かめておく。 例 7: 縦中横 ここで縦中横を見ておく。 青空文庫のテキストでは、こう書く。 あいう!?えお |29《二十九》[#「29」は縦中横] EPUB の中では、こうなる。 <p>あいう<span class="tcy"><span>!?</span></span>えお</p> <p><span class="tcy"><span><ruby>29<rt>二十九</rt></ruby></span></span></p> そして画面では、こう出る。 「!?」と「29」が、縦の 1 マスに収まる この class="tcy" に、例 6 の 4 行が効いている。 浅瀬 1: Play ブックスは、古い航路を使っていた C の本、つまり古い書き方を全部外した本を開いた。 Apple Books、Thorium、Kindle Previewer は変わらなかった。 縦書きも、ルビも、傍点も、縦中横も、頭に何も付かない標準の書き方で出た。 Google Play ブックスだけが、縦中横を崩した。 左: -epub-text-combine あり。右: 標準の text-combine-upright だけ 切り分けるために、古い書き方を 1 種類ずつ残した本を作った。 見る前に、予測を書いておいた。 「-webkit- だけ残した本は組まれる。-epub- だけ残した本は崩れる」 両方とも外れた。 -webkit- だけの本は崩れた。 -epub- だけの本は、縦中横が組まれた。 画面は、古い書き方を全部残した本と画素まで一致した。 例 6 の最後の行、-epub-text-combine である。 3.3 の時点ですでに「使うべきでない」とされている書き方である。 これは、標準の CSS だけにすればきれいに片付く、という予想を覆した。 もう一つあった。 <guide> を外した本は、Play ブックスで開くと表紙から始まった。 <guide> がある本は、本文から始まる。 例 5 で「これを消せばいい」と思った行である。 実は EPUB 3 には、<guide> の後継がある。 目次のファイル(nav.xhtml)の中の、landmarks という一覧である。 AozoraEpub3-JDK21 は、両方を出している。 <!-- EPUB 3 の書き方(nav.xhtml の中) --> <nav epub:type="landmarks"> <ol> <li><a epub:type="bodymatter" href="0001.xhtml">本文</a></li> </ol> </nav> <!-- EPUB 2 の書き方(package.opf の中。legacy) --> <guide> <reference type="text" title="sec0001" href="xhtml/0001.xhtml"/> </guide> どちらも「本文はここから」と言っている。 <guide> を外しても、landmarks は残っていた。 それでも Play ブックスは、表紙から開いた。 <guide> だけを戻すと、本文から開いた。 新しい港には新しい標識が立っている。 少なくとも今回の Google Play ブックスは、出発地点を決めるとき、古い標識に頼っていた。 海図には「使わない」と書いてあるのに、その標識がないと出発地点が変わった。 どちらも残した。 浅瀬 2: 目次の階層は、読まれない場所にあった 港に寄ったついでに、目次パネルも見た。 『銀河鉄道の夜』の目次はこうである。 Apple Books の目次パネル 章が並んでいる。 では、「第一部」の下に「第一章」がぶら下がるような、階層のある本はどうか。 4 つの港とも、平らだった。 AozoraEpub3-JDK21 の出力では、目次の階層は NCX という EPUB 2 の目次にしか書かれていなかった。 今回の 4 つのリーダーは、EPUB 3 の目次である NAV を使っていた。 誰にも読まれない場所に、階層を書いていた。 調べると、元の AozoraEpub3 は 2012 年の時点で、NAV にも階層を書いていた。 止めたのは、私である。 2025 年 12 月、AozoraEpub3-JDK21 で、NAV の階層を出さないようにした。 少し言い訳をしておく。 私がこの派生版を触り始めたのは、2025 年である。当時は、プログラム全体の構造も EPUB の仕様も、まだよく分かっていなかった。 それなのに、目次には悩まされていた。小説によっては目次が途中で切れる。目次のせいで、正しい EPUB が作れないこともあった。 そこで、こう考えた。 「もう、フラットにしちゃえ!」 階層をなくせば、ひとまず目次で本が壊れることは避けられる。そう思って直した。 元の書き方は、こうだった。 <li><a>第一部</a></li> <li><ol> ← 見出しの無い <li> の中に、子の目次 <li><a>第1章</a></li> </ol></li> 「第一部」と、その子の「第1章」が、別々の <li> に入っている。 NAV の規則では、子の目次は、親の見出しのすぐ後ろに置く。 この形は、NAV の規則に合わなかった。 本当は、その書き方を直せばよかった。 だが当時の私は、階層を出すこと自体を止めた。 規則には合うようになった。 目次は平らになった。 階層は NCX にだけ残った。 今回のリーダーが読まない場所にしか階層を残さなかったのは、私のバグである。 1 年近く、気づかなかった。 今になって過去の自分の修正を読み返すと、正直、ちょっと恥ずかしい。 今回は、規則どおりの書き方で戻した。 <li><a>第一部 旅立ち</a> <ol> <li><a>第1章 春の朝</a></li> </ol> </li> 親の <a> のすぐ後ろに、子の <ol> を置く。 違いは、<li> の閉じる位置だけである。 左: 今までの出力。右: NAV に階層を入れた出力 予定に無い寄港、4。予定に無い修理、1。 夜の修理: 全ページ画像の本 ■ 夜の修理 天候: 晴れ/検証器は 0 件 位置: 画像だけの本を直した。検証器は黙った 観測: Apple Books が、横長のページの右端を切っている 判断: 検証器の沈黙は、無事の知らせではない 針路: ページの大きさを本全体で 1 つにする ここで航海が、予定から大きく外れる。 AozoraEpub3-JDK21 は、小説だけでなく、画像をまとめた本も作れる。 漫画やスキャンした本のように、全ページが画像の本である。 小説の話をしていたので、見る予定は無かった。 ついでに 1 冊作って、検証器にかけた。 エラーが 12 件出た。 この本を港に持って行くと、Apple Books で絵の上下が切れていた。 Apple Books。左: 直す前(上下が切れる)。右: viewport を入れた後(絵の大きさがそろった本) Google Play ブックスでは、絵が出なかった。 Google Play ブックス。左: 直す前(絵が出ない)。右: 直した後 CI の EPUB は 0 warnings だった。 CI は、画像だけの本を作っていなかった。 これは検証器の失敗ではない。 検証する本の種類を、こちらが狭くしていた。 晴れていたのは、その海域に船を出していなかったからである。 直した。晴れた。切れていた 足りなかったのは、ページの大きさの指定(viewport)だった。 画像だけの本は、1 ページ 1 枚の絵を、決まった大きさのページに置く。 その大きさを書いていなかった。 ページごとに、その絵の大きさを書いた。 <meta name="viewport" content="width=1200, height=1600"/> 各ページの <head> に入る、この 1 行である。 検証器にかけた。 0 件。晴れた。 念のため、港でも見ることにした。 ただし普通の漫画の絵では、どこが切れているのか分かりにくい。 そこで、ページの縁に赤い枠とページ番号を描いた試験用の絵を作った。 縦長、横長、正方形、極端に横に長い絵を混ぜた。 赤い枠が 4 辺とも見えていれば、切れていない。 Apple Books で開いた。 1 ページ目より横に長いページの、右端が切れていた。 極端に横に長いページでは、ページ番号ごと見えなかった。 最初は「直す前も切れていたのだから、悪化ではない」と判断した。 これは誤りだった。 同じ赤い枠の本を、直す前の版と直した後の版で作って並べた。 直す前の版では、縦長と正方形のページの下が切れていた。 直した後の版では、1 ページ目より横に長いページの右端が切れていた。 切れるページと、切れる向きが入れ替わっただけだった。 それを「同じく切れている」で片付けていた。 そこで作り直した。 ページの大きさを、本全体で 1 つにした。 縦長の絵も横長の絵も、そのページの中に、縦横の比を保ったまま置く。 4 つの港とも、赤い枠が 4 辺とも見えた。 Apple Books、赤い枠の試験用の本。上: 直す前(縦長と正方形のページの下が切れる)。中: ページごとに大きさを書いた版(1 ページ目より横に長いページの右端が切れ、極端に横長の 5 ページ目は番号ごと見えない)。下: 本全体で 1 つにした版(4 辺とも見える。絵の並びは上・中と少し違う) 検証器の 0 件は、絵が切れていないことを教えない。 仕様に合うことと、人が開いてきちんと読めることは、別の試験だった。 天候、晴れ。ただし港の桟橋が、一つ足りない。 もう一度海図へ: 消しても変わらないことを測る ■ 再出発 天候: 晴れ 位置: 古い要素を、既定で出さないことにしたい 観測: 出しても出さなくても、4 つの港で何も変わらない 判断: 既定で外す。設定で戻せるようにする 針路: 本物の積荷で、もう一度 予定の航路の 2 行目「古い要素を消す」に、ようやく戻ってきた。 ただし、全部ではない。 浅瀬 1 で、<guide> は Play ブックスが使っていると分かった。 残りの 2 つ、表紙の <meta name="cover"> と、NCX の目次(toc.ncx)はどうか。 消す前に、消しても変わらないことを測った。 消した本と消さない本を 4 つの港で開き、見比べた。 表紙(本棚のサムネイル) 開く位置 目次 最初のページ どこも変わらなかった。 Apple Books と Play ブックスでは、最初のページの画面が画素まで一致した。 小さな浅瀬が一つあった。 Kindle Previewer の本の情報の画面は、toc.ncx が無い本でも「NCX: Specified(あり)」と表示する。 表示を見て「ある」「ない」を判断すると、間違える。 表紙の meta と NCX は、既定で出さないことにした。 古いリーダーのために、設定で戻せるようにした。 確かめようのない港(Kobo)向けの出力は、今までどおり出す。 出す前に: 本物の積荷で、もう一度 ■ 出す前 天候: 晴れ/試験は全部通る 位置: 実際の作品データで、もう一度 4 つの港を回る 観測: 規格とは別の不具合が 4 つ 判断: 試験用の本では、見つからなかったものばかり 針路: 直してから出す ここまで港に持って行ったのは、試験用に作った本だった。 でも、AozoraEpub3-JDK21 は、実際の作品を電子書籍にする道具である。 ならば、本物のデータでも試しておこう。 いくつかの作品データを EPUB に変換し、4 つの港で確かめた。 同じデータを前の版でも変換して比べた。今回の変更によって、本文や画像が欠けたり、目次の項目が失われたりする問題は見つからなかった。 ところが、以前から潜んでいた別の不具合が 4 つ見つかった。 その 1: 最初の話が、2 回始まる ある作品では、最初の話が EPUB の中に 2 回入っていた。 調べると、変換元に同じ話を指すリンクが 2 つあった。 同じものを 2 回たどって、2 回取り込んでいたのである。 本の先頭から同じ話が繰り返される。読む側からすれば、何が起きたのか分からない。 その 2: 「第三話」の下に、後の話が全部ぶら下がる 本文に「第三話」と書かれた行があると、目次の上でその行が親になり、後に続く話が全部その下に入ってしまう本があった。 目次を階層表示に戻したことで、表に出た問題だった。 平らな目次なら分かりにくい。階層にすると、どの見出しを親子として扱っているかが、はっきり見えてしまう。 NAV を直したから終わり、ではなかった。今度は、目次にする見出しそのものを確かめる必要があった。 その 3: Apple Books の目次が、特定の文字で壊れる 目次の項目名に & や < があると、Apple Books の目次パネルにエラーが出た。 項目が途中で途切れ、ページ番号まで消えてしまう。 ところが、EPUB の検証器は通る。 ここは、1 か所だけ変えた本を何冊も作って切り分けた。 &、<、]]> では壊れる > や引用符では壊れない 少なくとも、XML が単純に壊れていて本を開けない、という話ではなかった。 この版では、目次の項目名に限って問題を起こす文字を全角に置き換えた。本文の文字は変えていない。 表示を回避するための処置であって、Apple Books の内部で何が起きているかまで解明できたわけではない。 左: 項目名に &。右: 全角の & その 4: 「底本:」まで目次に出てくる 作品の末尾に付いていた「底本:」で始まる行が、見出しとして扱われていた。 しかも、一部の作品だけではない。調べた本の目次に、同じように現れていた。 本文の情報として残ることと、読者が選ぶ目次の項目になることは違う。 これも、試験用の本では気づかなかった。 4 つとも直した。 どれも、最初に用意した試験用の本では見つからなかったものだった。 本物の積荷は、試験の積荷より重い。 こうして v1.8.0-jdk21 を出した。 その夜、スマホで文字を大きくすると表題ページで題名と著者が重なる、という不具合も直した。 帰港 ■ 帰港 位置: v1.8.0-jdk21 を出した 予定の航路: 4 行 実際の航路: 下のとおり 出港前の予定は、4 行だった。 予定の航路 1. 仕様を読む 2. 古い要素を消す 3. 検証器が通ることを確かめる 4. 出す 実際の航路は、こうなった。 実際の航路 1. 仕様を読む → 消す理由が無かった。検証器は最初から 0 件だった 2. 4 つの港に寄る → Play ブックスだけ、古い縦中横の書き方と <guide> に頼っていた → 目次の階層を、今回のリーダーが読まない場所に書いていた(私のバグ) 3. 予定に無かった、全ページ画像の本の修理 → 検証器のエラー 12 件。直して 0 件。それでも Apple Books で横長のページが切れていた 4. 表紙の meta と NCX を消しても変わらないことを、4 つの港で測る 5. 本物の積荷で、もう一度 → 規格と関係ない不具合が 4 つ 6. v1.8.0 を出す 日本の縦書きの小説について、分かったことをまとめておく。 縦書き・ルビ・傍点は、頭に何も付かない標準の CSS で、4 つの港とも出た。古い書き方も、当面は並べて書いておく 縦中横は、Play ブックスがまだ「使うべきでない」とされる書き方に頼っている。だから残す 表紙の meta と NCX(EPUB 2 の名残)は、出さなくても 4 つの港で何も変わらなかった。既定で外し、設定で戻せる <guide> も EPUB 2 の名残だが、Play ブックスが開く位置に使っている。EPUB 3.3 の仕様では使わないはずなのに、実機のために今は残す 航海で分かったことは、3 つである。 海図(仕様の区分)は、港(リーダー)が何に頼っているかを教えない。 晴れ(検証器の 0 件)は、積荷が無事だという知らせではない。 本物の積荷には、試験用の本が教えてくれないことがある。 検証器が 0 件と言っていたのは、検証器に渡した本についてである。画像だけの本を渡していなかったのは、こちらだった。 前回の RAW の話では「一番大きい JPEG を探せばいい」という前提が崩れた。 今回は「仕様どおりなら読めるはずだ」という前提が崩れた。 どちらも、実物を開かなければ分からなかった。 ここまで書くと、ひどい目に遭ったように見えるかもしれない。 でも、面白かった。 お客さんや営業が、後ろに立っていないからである(笑)。 仕事なら「いつ直るの?」と聞かれそうなところでも、今回は「なんでこうなるんだろう?」と寄り道できる。予想が外れたら、別の本を作って、また試せばいい。 昔の自分が直したはずのところで転んだのは、少し恥ずかしかったけれど。 港にも事情がある 4 つの港を回って、もう一つ思ったことがある。 リーダーを作っている人たちも、大変なのだろう。 EPUB の仕様は更新されていく。 一方で、一度出版された電子書籍は、そのまま何年も、何十年も残る。紙の本と同じで、古いからといって、読む人の本棚から消えるわけではない。 読む側にしてみれば、昨日まで読めていた本が、リーダーの更新で急に読めなくなったら困る。 新しい仕様には対応しなければならない。でも、昔の本も読み続けられるようにしたい。 この両立は、なかなか難しいはずだ。 今回見つけた違いが、すべて互換性のために意図されたものとは限らない。単に未対応のところも、不具合もあるだろう。各社の事情までは分からない。 それでも、Google Play ブックスが古い指定を使っていたことは、最初とは少し違って見える。 そういうこちらも、最初は古い要素を消すつもりで出港して、結局、いくつか残して帰ってきた。 船を作る側にも、港を守る側にも、事情がある。 そういえば、EPUB の中身は ZIP だった。 もし手元に、自由に開ける EPUB があったら、一度中をのぞいてみてほしい。 本文の HTML や CSS が、そこに入っている。 普段何気なく読んでいる電子書籍も、こうして作られている。 EPUB 3.4 は、まだ草案である。 海図は、これからも書き換わる。 そのときは、また港に寄ってみよう。 短い航海の予定だった。 資料: 海図の細かい文字が気になる人へ ここからは、試した環境と原典である。物語はもう帰港している。 寄った港の版: Apple Books(macOS)、Google Play ブックス 2026.10.2.1(Android)、Thorium 3.5.1(Windows)、Kindle Previewer 4.0.3.0(Windows。Tablet・Phone・E-reader)。v1.8.1-jdk21 を出した後、Send to Kindle 経由で Kindle 端末と iPhone の Kindle アプリでも確かめた 寄っていない港: Kobo(端末が無い)。Kobo 向けの出力は今までの形を残している 検証器: epubcheck 5.3.0 と 5.4.0 EPUB の仕様(W3C) EPUB 3.3(2026 年 1 月 13 日の勧告版) 5.9 Legacy features: 表紙の meta、<guide>、NCX 6.1.4.3.2 The rp element 6.3.1.3 Prefixed properties(-epub- で始まる CSS) Expressing the ICB in XHTML(画像だけの本の viewport) EPUB Reading Systems 3.3 5.7 Legacy features(「Reading systems MUST NOT support legacy features……」) EPUB 3.4(2026 年 11 月 2 日の候補勧告草案。最新版) EPUB Reading Systems 3.4 EPUB 3 Overview 3.4 の予定: Publishing Maintenance Working Group Charter(「2027 February: EPUB 3.4 final version」) EPUB が借りている、ウェブの仕様 CSS Writing Modes Level 3(縦書き。縦中横は text-combine-upright) HTML の ruby 要素(<rt> と <rp>) 日本語組版処理の要件(W3C。縦書き・ルビ・縦中横などを、日本語の組版の側から整理した文書) 道具と作品 EPUB 3.3 と 3.4 への対応(要素ごとの一覧と、リーダーごとの違い) 最新版(v1.8.1-jdk21) AozoraEpub3(hmdev 氏の原作) AozoraEpub3-JDK21 青空文庫の注記一覧([#…] や 《》 の書き方) 例に使った作品: 宮沢賢治『銀河鉄道の夜』(青空文庫)

  3. today is a sad day for the web, and for a11y. but on the bright side, hopefully this is the last time i'll have to explain why buttons and controls should not have `cursor: pointer` alas, it is tiring trying to help you dorks. csswg-drafts#14251

    53 likes · 5 reposts · 5,448 views
  4. Why your INP can fall after one more click By Aditya Reddy Say you have a page with 49 interactions. The slowest took 480 ms, followed by 320 ms and 240 ms. Everything else was faster. Your INP is 480 ms. Now click something fast. You have changed no code, and that slow interaction still takes 480 ms. The score for the visit drops to 320 ms anyway. That's the part of INP I wanted to explain in my Event Timing specification PR. The PR is still open. A single number on a dashboard hides quite a bit of bookkeeping. What happened at click 50? INP skips one longest interaction for every complete group of 50 interactions. At 49, it picks the longest. At 50, it picks the second longest. The INP guide explains the reason for excluding these outliers on pages with lots of interactions. For our example, assuming all the extra interactions are below 240 ms: | Interactions so far | Longest values skipped | INP candidate | | --- | --- | --- | | 49 | 0 | 480 ms | | 50 | 1 | 320 ms | | 99 | 1 | 320 ms | | 100 | 2 | 240 ms | So you can cross two of these boundaries and go from 480 to 240 ms without fixing the 480 ms interaction. Nice dashboard, same slow click. The selection code in web-vitals amounts to this, with the latencies sorted from longest to shortest: const skip = Math.floor(interactionCount / 50); const index = Math.min(skip, descendingLatencies.length - 1); const candidate = descendingLatencies[index]; You need at least one candidate for this fragment. It is only the selection step, not a complete collector. web-vitals retains up to ten of the longest interactions, so the list can be much shorter than the interaction count. Count interactions, not event entries There is another place to get the numbers wrong before you even reach that selection. A click can leave several PerformanceEventTiming entries. Take these made-up values from one visit: | Interaction ID | Event | Duration | | --- | --- | --- | | 7 | pointerdown | 320 ms | | 7 | pointerup | 296 ms | | 7 | click | 280 ms | | 14 | click | 480 ms | | 21 | keydown | 240 ms | The first three rows belong to one interaction. Its latency is 320 ms, the longest duration in that group. Adding them gives 896 ms, but those durations overlap. Five rows here give us three interactions, with latencies of 320, 480 and 240 ms. Group by the nonzero interactionId. Leave entries with ID zero out of this calculation. The Event Timing spec describes how those IDs group related events. Your observer may have missed some of it For entries of type event, the default duration threshold is 104 ms. You can register an observer with durationThreshold: 16 to receive shorter events going forward. That does not bring back all the fast events from before registration. Even with buffered delivery, you only get the earlier entries that were kept under the default threshold. This is covered in the spec's measurement section. That's why the total interaction count and your observed list length need to stay separate. A fast interaction can increase the count without appearing in that list. Lowering the threshold late won't recover its duration. I would use web-vitals for production collection. There is first-input handling, page visibility and back/forward cache restoration to account for too. Code running in one window also cannot see every interaction in an embedded frame. When debugging the score, keep the selected interaction's ID and latency along with the total count. If the score drops, check which interaction it picked. And if you are fixing that 480 ms click, compare the click itself before and after the change under similar conditions. The proposed spec section puts the grouping, selection, observer threshold and retained-list limit together. The fast click at interaction 50 is a useful example of why those details matter.

  5. you are all so fucking dumb 🤦‍♂️ csswg-drafts#14251

    28 likes · 3,518 views
  6. buttons, selects and checkboxes are getting the hand cursor by default. interesting 🎭 csswg-drafts#14251 t.co

    7 likes · 332 views
  7. The debate is over, and evil has won. `cursor: pointer` now means clickable. RIP my ability to tell whether ⌘-clicking something will open it in a new window. csswg-drafts#14251

    81 likes · 9 reposts · 16,546 views
  8. Cursors for base appearance. CSSWG settled the long-standing debate: form controls with appearance: base get cursor: pointer when clickable, grab on range sliders, and not-allowed when disabled. #css #html csswg-drafts#14251

    86 likes · 21 reposts
  9. Reddit · · r/css · u/objectorientedman · w3c/csswg-drafts

    transform-inverse() and transform-transpose() I propose a transform-inverse and transform-transpose function to be added to CSS. It could greatly reduce the complexity of some 3D scenes, like ones with billboards, and would allow correct interpolation in animations

    1 point
  10. Правый клик с памятью: почему я храню счётчик в id пункта меню Chrome Мои хобби-проекты - небольшие расширения для Хрома и сайты-микросервисы. Одно из расширений, PDF to PNG, внезапно забралось на первое место в поиске Chrome Web Store по запросу “pdf to png” (ну, почти всегда), и им пользуются аж под 500 человек - это потрясающе! Раз PDF в картинки получилось, немедленно загорелось сделать и наоборот - картинки -> PDF, только ещё круче: чтоб работало через правый клик по картинке на любой странице, чтоб была лёгкая(!) возможность настроек - и чтоб всё прям локально в браузере и картинки при этом никуда не загружались! Все замечательные настройки поместил в сайд-панель, вот только обычный пользователь может же никогда не догадаться её открыть, а таки хотелось бы дать о ней знать, но при этом не дёргать тех, кому достаточно меню правой кнопки мыши - что ж, будем показывать её иногда (ну или пока пользователь явно не кликнет куда-то в панель). Эта хотелка упёрлась в правило Хрома: открыть панель можно только в ответ на клик, а в обработчике клика по пункту меню это значит - до первого await. А чтобы показывать иногда, на каждом клике расширение должно помнить, сколько раз уже её показало, - а прочитать это из хранилища и есть await. Решение - хранить счётчик прямо в id пункта контекстного меню! :) И вот - Save Image as PDF. Правый клик - и картинка уже PDF. Правый клик по картинке → Save image as PDF Await страницы и await воркера по-разному “сжигают” жест Правый клик случается на чужой странице, где от расширения ничего не открыто: ни панели, ни своих страниц. Соответственно, клики по пункту меню слушает сервис-воркер. Наивный подход - прочитать в обработчике клика, сколько раз панель уже показывали, и по этому решить, открывать ли её: chrome.contextMenus.onClicked.addListener(async (info, tab) => { const { shown = 0 } = await chrome.storage.local.get('shown'); if (shown < 3) { chrome.sidePanel.open({ tabId: tab.id }); } }); sourcecraft.dev Панель, конечно же, не откроется ни разу, а в консоли воркера на каждый клик будет ошибка sidePanel.open() may only be called in response to a user gesture. Некоторые API Хрома, например chrome.sidePanel.open() и chrome.permissions.request(), работают, только если их вызвали в ответ на действие человека: клик, нажатие клавиши. Клик по пункту меню таким жестом считается, вот только живёт этот жест на странице и в воркере очень по-разному. На обычной странице работает transient activation: после клика жест “живёт” 5 секунд. Так прямо и записано в коде Chromium, kActivationLifespan = base::Seconds(5), с пояснением “long enough to allow network round trips even in a very slow connection”. Проверить легко - вставьте в консоль DevTools на любой странице и кликните по ней: document.onclick = () => [4000, 6000].forEach((ms) => setTimeout(() => console.log(ms, navigator.userActivation.isActive), ms)); // 4000 true // 6000 false sourcecraft.dev А в воркере никакого таймера нет: жест держится, только пока Хром передаёт событие клика обработчику. async-обработчик на первом же await возвращает управление Хрому - и жеста больше нет. Даже await Promise.resolve(), который не ждёт вообще ничего, его сжигает. Сам await chrome.sidePanel.open() при этом можно - главное, чтобы перед ним ничего не ждали. В команде Chrome об этой разнице знают: в WebExtensions CG предлагают дать расширениям жест по времени, как на страницах, но пока это только предложение. Почти любое полезное действие требует await: прочитать настройки, скачать картинку, собрать PDF. К тому же воркер мог только что проснуться от этого самого клика: в MV3 его выгружают примерно после 30 секунд простоя - так себе детерминизм. После такого холодного старта в памяти пусто, так что и глобальная переменная не спасёт. Как же тогда узнать в момент клика, сколько раз панель уже показывали? Колбэк вместо await На chromium-extensions это можно обходить старым добрым колбэком, ибо вот именно для одного колбэка Хром оставляет “жест”, но только для одного, и только для колбэка (никаких await!). Колбэк внутри колбэка жеста уже не получит. chrome.contextMenus.onClicked.addListener((info, tab) => { chrome.storage.local.get('shown', ({ shown = 0 }) => { if (shown < 3) chrome.sidePanel.open({ tabId: tab.id }); // так сработает если колбэк именно один - без вложенности }); }); sourcecraft.dev Однако получилось придумать другой трюк и остаться в мире await! Счётчик в id пункта меню Я придумал такое правило, чтобы насильно наверняка познакомить пользователя с сайд-панелью, но не мозолить глаза постоянно: открывать панель на первых трёх сохранениях, потом на каждом десятом, пока пользователь не ткнёт хоть что-то в панели или не откроет её сам (ну то есть - панель точно была замечена). Для этого уже в момент клика нужно знать, какое это по счёту сохранение и какой режим выбран в настройках. Есть одно своё значение, которое приходит в обработчик клика по меню синхронно, - это, собственно, info.menuItemId, id пункта, по которому кликнули. Это может быть неочевидным, но вообще-то ничего не мешает пересоздавать пункты меню хоть двадцать раз в день. Вот туда состояние и легло: // id = 'save|<step>|<mode>' // step 0..12 пользователь ещё не пользовался панелью; считаем сохранения через меню // known пользователь открыл панель сам или что-то в ней сделал; больше не меняется // mode download | panel | collection export const NUDGE_SAVES = 3; export const LAST_STEP = 12; export const KNOWN = 'known'; const ID_PATTERN = /^save|(known|0|[1-9]\d?)|(download|panel|collection)$/; export function parseMenuId(id: unknown): MenuState | null { … const match = ID_PATTERN.exec(id); … return { step, mode: match[2] as MenuMode }; } export function shouldOpenPanel(state: MenuState): boolean { if (state.mode === 'panel') return true; if (state.step === KNOWN) return false; return state.step < NUDGE_SAVES || state.step === LAST_STEP; } export function afterMenuSave(state: MenuState): MenuState { const { step } = state; if (step === KNOWN) return state; return { ...state, step: step === LAST_STEP ? NUDGE_SAVES : step + 1 }; } sourcecraft.dev Пункт меню у расширения один, с одним и тем же заголовком “Save image as PDF”, меняется только id: save|0|download, save|12|collection, save|known|panel и т.п. Обработчик разбирает id и, если надо, открывает панель первым же вызовом, ничего не дожидаясь: export function handleMenuClick(info, tab, deps): Promise<void> { const state = parseMenuId(info.menuItemId); // синхронно … const openedPanel = shouldOpenPanel(state) && tabId !== undefined; if (openedPanel) { deps.openPanel(tabId).catch(warn('sidePanel.open')); // chrome.sidePanel.open({ tabId }) } … } sourcecraft.dev После клика, уже вне жеста, воркер вычисляет следующее состояние и перерегистрирует пункт: contextMenus.removeAll() и create() с новым id. contextMenus.update() тут не поможет: менять id он не умеет. Саму регистрацию меню Хром хранит сам, она переживает и выгрузку воркера, и перезапуск браузера. А вот при обновлении расширения Хром меню стирает, поэтому копия id лежит ещё и в storage.local, из неё пункт и восстанавливается. Конечно, в id много не положишь - счётчик да режим. Зато до открытия панели не читается вообще ничего; это чуть быстрее чтения chrome.storage, хотя в UX разницы не заметить. Если у вас тоже расширение с динамическим меню Если вам тоже хочется, чтобы правый клик вёл себя по-разному, вот что у меня в итоге сработало: вызов, которому нужен жест, ставьте первой строкой обработчика, до любого await; всё, что нужно знать в момент клика, кладите в id пункта меню, а после клика перерегистрируйте пункт с новым id; ну, вообще-то и вариант с колбэком тоже сработает Само расширение - Save Image as PDF в Chrome Web Store, у него есть и сайт. А если нужно наоборот, PDF в картинки, - есть PDF to PNG, с которого всё и началось (сайт). А у вас? А вы как передаёте состояние в onClicked? Если нашли способ проще id и не завязанный на единственный колбэк - расскажите. P.S. Впервые опубликовано в моём блоге agilesoftwaredevelopment.com.

Trending awards, every completed day and week
Gold
11st place finishes
Silver
12nd place finishes
Bronze
03rd place finishes
Awarded repositories
1Owned by w3c now

Repositories

Ordered by gold, then silver, then bronze.

  1. csswg-testBest: 1st by PR authors, 23 PR authors110

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. #1 by PR authorsDaily trending
    23 PR authors
    csswg-test
  2. #2 by PR authorsDaily trending
    25 PR authors
    csswg-test

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.