# Known differences between the native and the web target

- **Date:** 2026-11-06
- **Related documents:** [PLAN.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/PLAN.md) (section 10, point 5), [layout spec](https://surface.solenovis.com/docs/layout.md), [Phase 2 plan](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/plan/phase-2.md) (P2.3, P2.4), [ADR-0131](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/adr/0131-one-behavior-on-all-targets.md)

The native target paints with its own layout, renderer and text stack. The web target uses the browser. This document lists each known difference that the eye can see. The conformance suite (`tests/layout`) uses a test font with fixed metrics, so it does not see the differences of text.

Version 1 has one behavior on all targets (ADR-0131). The column `Plan` gives the plan of each row:

- "1.0 (ADR-0131)": version 1 removes the difference. The ADR gives the standard for all targets. The code step removes the row when its fix is in.
- "Keep": the difference stays. Most such rows are a limit of a platform, and ADR-0131 gives the reason. A row with another reason names it in the column `Plan`.

## How to compare

1. Run the paint test. It lays out and paints `examples/reference` in 1200 × 900 logical px with the system font, and writes the image to `target/tmp/reference-native.png`:

   ```bash
   cargo test -p spike-apps --test paint
   ```

2. Open the same app on the web (`spike/web/index.html?app=reference`) in a window of the same size, in the light theme, and take a screenshot. The web page has a link bar above the app and a margin, so its boxes start at (16, 50).

## Differences

| Difference | Web (Chromium on macOS) | Native | Cause | Plan |
|---|---|---|---|---|
| Edges of shapes (ADR-0029) | The browser anti-aliases each shape with its own approximation of the area. | Coverage masks with the exact area of each pixel, also where two parts of a stroke overlap (ADR-0131). A curve becomes lines with at most a quarter of a device pixel of distance from it. In the plot of `tests/fixtures/drawing`, 0.16 % of the pixels of Chromium differ by more than 32 of 255, at the edges of curves. No pixel differs by more than 128 (2026-11-05). | Each browser computes the coverage of a curve in its own way, and CSS cannot choose it. | Keep: the anti-aliasing of the browser (ADR-0131) |
| Transitions (ADR-0035) | A theme change and a new window width animate each property with a `transition`. | A theme change or a new window width applies at once. A press hits the shown position of a transform, as on the web (ADR-0131). | The style pass of a new environment computes each style again, and it has no old style to animate from. | 1.0 (ADR-0131) |
| The words of `split-words` (ADR-0153) | `Intl.Segmenter` of the browser, with the ICU and the dictionaries of the browser. | The word break iterator of the ICU of the system. | The browser and the system can have different versions of ICU and of its data. | Keep: the data of the platform |
| The letters of `split-letters` (ADR-0153) | `Intl.Segmenter` with the version of Unicode of the browser. | `unicode-segmentation` with its version of Unicode. A new emoji can be one letter on one target and two letters on another. | The versions of Unicode differ until both have the same version. | Keep: the data of the platform |
| The parts of a split text with a `Span` in it (ADR-0153) | The copy of the parts has the font and the color of the `Text`, not of the `Span`. | Each part paints its glyphs with the font and the color of its run. | The copy of the web has one element for each part. | 1.0 (ADR-0131) |
| The ligatures of `split-letters` (ADR-0153) | `font-variant-ligatures: none` on the text: its layout has no ligatures. | Each letter paints alone, but the layout of the text keeps its ligatures, so a font with ligatures places the letters a little differently. | The text system of the native targets has no switch for the ligatures yet. | 1.0 (ADR-0131) |
| A computed part during an enter or an exit (ADR-0104) | The `transform` of `enter` or `exit` replaces the whole property. | A computed part stays on top of the value of `enter` or `exit`. | A style attribute comes after the rules. | 1.0 (ADR-0131) |
| `on:visible` at the edge (ADR-0106) | The browser computes the area from its own rounding of the boxes. | The area comes from the boxes of the layout, before pixel snapping. A difference of less than 1 px can come one frame earlier or later. | Each target has its own boxes. Only one layout engine for all targets can remove this, and the web uses the browser (ADR-0131). | Keep |
| The scheme of `href` (ADR-0109) | The element gets no attribute for a scheme that can run a script. | The value stays, and a press on the `Link` does nothing. A `Link` with `http`, `https`, `mailto` or `tel` opens the browser of the system, and a relative URL goes to the router of the app. | A native target has no script in a URL. | Keep |
| Theme choice (ADR-0044) | `data-theme` on the root element chooses light or dark. | The system chooses. | The native targets have no such attribute. | Keep |
| The theme of the system (ADR-0103, ADR-0122) | `prefers-color-scheme` and `prefers-contrast` select the variant before the first paint. | macOS reads both settings. Linux reads `color-scheme` and `contrast` of the settings portal, and follows each change. The first frame on Linux can have the light theme, while the bus starts the portal. Without a portal, the light theme. Windows reads `AppsUseLightTheme` and high contrast before the first frame, and follows each change. In high contrast, the color of a window of the theme chooses the dark or the light variant. | The read of the portal does not stop the first frame, because the start of a portal can take some seconds. | Keep |
| The text size of the system (ADR-0085, ADR-0122) | The default font size of the browser scales each length in `rem`. | On Linux, the `text-scaling-factor` of GNOME from the settings portal scales the whole UI, also borders. Other desktops have no such factor. macOS has a text size for each app in its settings of accessibility. No public API gives it to other apps (the Apple developer forums, 2026), so macOS has no factor. On Windows, "Make text bigger" (`TextScaleFactor`, 100 % to 225 %) scales the whole UI. | A native target scales the whole UI (ADR-0085). | Keep |
| The input method on Windows (W6 of [windows.md](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/plan/windows.md)) | The browser gives the input method the text of the field. A reconversion of committed text works, and an input method can use the text around the caret for its suggestions. | The context of the Text Services Framework holds only the text of the composition. A reconversion of committed text does not work, and an input method does not get the text around the caret. | The shell uses the text store of TSF, which keeps the text only during a composition. A text store of the shell (`ITextStoreACP`) can give the whole text. | 1.0 (ADR-0131) |
| The cursors of CSS that a system does not have (ADR-0160) | Each browser shows the cursor of its system, or an image of its own for a cursor that the system does not have. | Each shell shows the cursor of its system, or the nearest cursor that the system has. Windows: `grab` and `grabbing` show the arrows of a move (`IDC_SIZEALL`), `context-menu`, `alias`, `copy`, `zoom-in` and `zoom-out` show the arrow, `cell` shows the cross, and each side of `*-resize` shows the arrows of its axis. macOS: `help`, `progress`, `wait`, `move` and `all-scroll` show the arrow, and `cell` shows the cross. Before macOS 15, `zoom-in` and `zoom-out` show the arrow, and `col-resize` and `row-resize` show the arrows of an axis. Linux: the cursor theme of the user gives each cursor. The X11 window of ADR-0088 shows the arrow for each cursor. | A system has its own set of cursors, and a native app has no cursor images of its own. | Keep: a limit of the platform (ADR-0160) |
| The formats of an `Image` (ADR-0033, ADR-0160) | The browser shows each format that it knows, for example AVIF. | The native targets show PNG, JPEG, WebP, GIF and SVG. Another format gives `error` with the case `format`. | Surface ships no other decoder, and a decoder in each app is a new dependency (`AGENTS.md`, rule 9). | Keep |
| The case of `error` of an `Image` from another origin (ADR-0160) | The runtime reads the file again to find the case. The browser blocks this read for a file of another origin without CORS, so such a file that did not show gives `network`. | The case comes from the data of the file. | The browser applies CORS (ADR-0037). | Keep |
| The sizes of `resize` (ADR-0160) | The browser gives the size of its own box. | The size of the box of the layout. | The boxes of the browser and of Taffy can differ by up to 0.5 logical px (layout spec section 11), as for `on:visible` at the edge. | Keep |
| `line-clamp-` on a `Text` with parts (ADR-0111) | The box of a part holds the lines that the clamp hides. | The box of a part holds only the lines that show. The box of the `Text` is the same. | The native paragraph has no hidden lines. | 1.0 (ADR-0131) |
| `no-underline` on a `Span` in an underlined `Text` (ADR-0111) | The line of the `Text` goes on under the `Span`, as CSS draws a line of an ancestor. | The `Span` has no line. | Each run has its own `text-decoration` on the native targets. | 1.0 (ADR-0131) |
| The time at which a module constant gets its value (ADR-0118) | When the page loads, in the order of the modules. | At the first read. A server has one value for each thread. | The native code has no step that runs before the first component. A constant with a value that is not a literal is a value of the thread. | 1.0 (ADR-0131) |
| The order of strings (ADR-0113) | `<`, `sort` and `sortBy` compare by UTF-16 code units, as JavaScript does. | They compare by Unicode scalar values, also in `surf test` and on a server. Some emoji and rare characters sort in another order. A character above U+FFFF, for example most emoji, comes before a character from U+E000 to U+FFFF on the web. On a native target, it comes after that character. | UTF-16 code units are the order of JavaScript. On the web, a comparison by scalar values costs a function call for each comparison. | Keep: a choice of the project owner for speed (ADR-0113) |
| HTTP (ADR-0037) | The browser applies CORS, sets some headers itself, and hides some headers of a response from another origin. | No CORS. An app sees each header. | The rules of the browser. | Keep |
| Scaled images (ADR-0033), also a background image with `bg-cover` or `bg-contain` (ADR-0127) | The browser scales with its own filters. | The CPU averages the pixels for a smaller image and mixes the four nearest for a larger one. | Each browser chooses its own filter, and CSS cannot choose it. So no native filter can match all browsers (ADR-0131). | Keep |
| An animated WebP on Windows (ADR-0033, ADR-0131) | The browser plays it. | The first frame, when the system has the WebP codec. Windows Server has no WebP codec, so it shows nothing there. An animated GIF and an APNG play on each target, and an animated WebP plays on macOS and Linux. | The WebP codec of Windows (the app "WebP Image Extensions") gives no frames of an animation and no delays. Only a decoder of WebP in each app can remove this, and that needs a new dependency (`AGENTS.md`, rule 9). | Keep: a limit of the platform |
| Glyph edges | Chromium on macOS: glyph positions at a quarter of a pixel, and linear coverage. At 10, 11, 12, 14, 16 and 18 px, some glyphs of SF are up to 0.5 px taller than their outlines. An example is `l` at 12 px. Chromium on Windows: the coverage grows with the luminance of the text. At 12 px, DirectWrite moves the edges of a glyph to whole pixels. Chromium on Linux: not measured. | Glyph positions at a quarter of a device pixel. The coverage is linear. On Windows, it has the table of Chromium for each of 8 steps of the luminance of the text. Each glyph has the height and the edges of its outline. | The rasterizer of the platform changes the height and the edges of a glyph at some sizes. ADR-0131 has the measurements of 2026-11-04 and 2026-11-05. | 1.0 (ADR-0131) |
| Redraw regions of the inspector (ADR-0105) | The box of each element that a batch changed. A region is an estimate, and one batch can give many regions. | The damage of the frame: one rectangle, the area that the shell paints again. | A browser does not tell a page what it paints. | Keep |
| Frame timings of the inspector (ADR-0105) | `update` is the time of the batches. `render` is the time from the animation frame to the next task, which holds style, layout and paint of the browser. Only a frame after a batch counts. | `update` is the time of the events. `render` is the time of the frame in the dev host: tasks, layout and paint into the scene, without the GPU. The dev host runs in the interpreter, so the times are larger than in a release build. | The browser does this work itself. | Keep |
| A case without values in a part of a value, in the inspector (ADR-0105) | A case in a record or a list shows as text: `"idle"`. | `.idle`. | The web target writes such a case as a string, and the compiler gives the agent only the type of the whole value. | 1.0 (ADR-0131) |
| The formats of `Locale` (ADR-0132) | `Intl` of the browser formats the values of `Locale`, and it compares strings, with the data of the browser. Chromium and WebKit write U+0020 before AM and PM in a time, for example `1:00:00 PM` in `en`. | The ICU of the system formats them with the data of the system (ADR-0132). ICU 72 and later write U+202F before AM and PM. The long time has the name of the time zone of the data of the system, for example `GMT+1` or `MEZ`. | Each browser and each system has its own version of ICU and of CLDR. A newer CLDR can change a pattern, a name or a space, mostly in the dates, the times and the names of time zones. On 2026-11-06, `en` and `de` gave the same text in Chromium, in WebKit, on macOS 27 and on Windows Server 2025. This was true for the numbers, the percents, the currencies, the relative times, the lists, the plurals, the order of strings and the dates. | Keep |
| The words of `split-words` (ADR-0153) | `Intl.Segmenter` of the browser finds the words with the data of the browser, with a dictionary for each language that needs one. | The word break iterator of the ICU of the system finds the words, with the dictionaries of the system (ADR-0132, ADR-0153). | The browser and the system can have different versions of ICU, so a few words can differ. | Keep: the data of the browser and of the system |
| The formats of `Video` and `Audio` (ADR-0155) | Each browser plays its own formats. A build of Chromium, for example the Chromium of Playwright, plays no H.264 and no AAC. | Each media system plays its own formats. AVFoundation on macOS plays no WebM. A default Ubuntu plays no H.264 and no AAC, unless the `.deb` of the app installs `gstreamer1.0-libav`. Windows needs a free extension of the Microsoft Store for VP9 and AV1. ADR-0155 has the table. | Surface ships no decoder. The system chooses its decoders and the patent licences of its formats. | Keep |
| The picture of `metadata` in the media controls of the system (ADR-0156) | The browser shows a picture from a path or a URL. | Windows shows a picture only from a URL with `http` or `https`. The call of `RandomAccessStreamReference` for a URL reads no path of a file. | A path needs the file API of WinRT, with its asynchronous calls. | 1.0 |
| An HDR video (ADR-0155) | A browser can show an HDR video on an HDR screen. | The frames are 8-bit sRGB, so an HDR video shows in SDR. | The renderers paint in sRGB. | Keep: HDR comes after version 1.0 (ADR-0155) |
| The parts of WebVTT in a track from a URL (ADR-0157) | The browser shows the regions, the vertical text and the styles of the file. | They show as plain text at the bottom of the video. In a literal file, they are a compile error. | Version 1 has a part of WebVTT on the native targets. | Keep: a part of WebVTT in version 1 (ADR-0157) |
| The caption style (ADR-0157) | The browser draws the text cues. A browser applies the caption style of the system, or caption settings of its own. | The runtime applies the caption style of the system on macOS and on Windows. Linux has no such setting, so the text cues have the default look of WebVTT. | A page cannot read the caption style of the system, so the browser chooses the look of the text cues. | Keep |
| The candidate of `srcset` (ADR-0159) | The browser chooses. It can take a smaller file, for example to save data. | The smallest density that is equal to or larger than the scale of the window, else the largest density. | HTML lets the browser choose. | Keep |
| The distance of `loading="lazy"` (ADR-0159) | Each browser chooses its distance, also from the speed of the network. | One height and one width of the visible area of the nearest `Scroll`, `List` or window. | HTML gives a page no way to set the distance. | Keep |
| The snap of a wheel event in pixels (ADR-0128) | The runtime snaps a step that passes a snap point, and a gesture of one wheel event that the browser does not snap (layout spec 5.12). Each other gesture keeps the snap of the browser. So a short wheel event of the browser tests goes back to its start in Chromium, and on to the next snap point in WebKit and Gecko. With `snap-proximity`, WebKit on Linux also snaps a step to a point farther than 0.3 of the visible area. | The shell tells the runtime the input and the end of each gesture, and the rules of layout spec 5.12 apply. | A browser gives a page no phase of a wheel event. So the runtime cannot tell a step of a wheel from a move on a trackpad, and a trackpad keeps the snap of the browser. | Keep |
| The hooks of SurfWave in a desktop app (wave spec 2) | A server runs `before` and `after` of `hooks.server.surfs` for each request. A request is the first page, the data of a page, a form, an endpoint or a remote function. A cookie of the server counts from the next request. | A desktop app runs no hooks. It runs the `load` functions and the remote functions of the server in its own process. A cookie of the server counts from the next navigation. | A desktop app has no server, so it gets no requests. | Keep: a desktop app has no server |
