Components

Four components ship in the package: Scroller, Portal, Tooltip and Popover. They're plain .svelte files, and your loader compiles them like your own components:

<script>
  import Scroller from 'gpuix-svelte/components/Scroller.svelte';
  import Portal from 'gpuix-svelte/components/Portal.svelte';
  import Tooltip from 'gpuix-svelte/components/Tooltip.svelte';
  import Popover from 'gpuix-svelte/components/Popover.svelte';
</script>

Scroller

GPUI paints no scrollbar, so Scroller is a scroll column with a drawn thumb. It measures its painted bounds and scroll offset, draws the thumb in a gutter and drags it on an overlay. GPUI doesn't capture the pointer, so a move with no button held counts as the release.

Props:

Colours come from var(--scroller-thumb) and var(--scroller-thumb-hover), with greys as fallbacks.

Virtual lists

A plain scroll column lays out and paints every child on every frame, on screen or off, so a long list slows down in proportion to its length. With virtual, Scroller renders GPUI's native <virtual-list>, and GPUI lays out and paints only the rows near the viewport.

The thumb then measures in rows, from getListScrollTop and the visibleRange event, and a drag scrolls with scrollToItem. In Substrate, a second-brain app built on this renderer, a timeline of 50 cards went from ~24 ms to ~1.4 ms per frame.

Portal

Paint order is document order, so a modal, toast or menu would otherwise have to be the root's last child. <Portal> renders from wherever the overlay is needed and still paints on top. The renderer moves only the native node to the root, so Svelte's {#if} blocks and teardown work as usual. The wrapper covers the window without a hitbox of its own, and its children position against the window and stay clickable:

{#if confirming}
  <Portal>
    <div class="scrim" onclick={() => (confirming = false)}>
      <div class="dialog" onclick={() => {}}>…</div>
    </div>
  </Portal>
{/if}

A later portal paints over an earlier one.

Tooltip

A hover tip beside its trigger, built on <anchored>. It shows after delay ms of hover and stays open while the pointer crosses onto the tip, with an 80 ms grace for the gap. The tip takes no clicks, so they land on whatever lies beneath it.

Props:

Colours come from var(--tooltip-surface) and var(--tooltip-ink), which you set for the whole app with set_css_vars.

A decorative trigger (text, an icon, a badge) needs nothing more, because the wrapper hears the hover through hitbox="self". A trigger with a listener of its own keeps its hitbox, which hides the wrapper's, so it spreads the handlers the children snippet receives:

<Tooltip content="Save (⌘S)">
  {#snippet children(hover)}
    <div class="btn" {...hover} onclick={save}>Save</div>
  {/snippet}
</Tooltip>

Popover

A controlled panel beside its trigger. You own open, and the trigger toggles it. onclose fires on a press outside both the panel and the trigger (any mouse button) and on Escape. A press on the trigger is left to the trigger's own click, so it closes the panel without reopening it.

Props:

Colours come from var(--popover-surface), var(--popover-border) and var(--popover-ink).

<Popover open={menu} onclose={() => (menu = false)}>
  <div class="btn" onclick={() => (menu = !menu)}>Menu</div>
  {#snippet content()}
    <div class="item" onclick={rename}>Rename</div>
  {/snippet}
</Popover>

Tooltip and Popover both wrap the trigger in a box that hugs it, at the start of whatever space the parent gives them, and hang their layer off that box.

<anchored>

For anything else that should sit beside a trigger, GPUI's native <anchored> element positions its child relative to the element it's rendered in. It hangs off its parent's box, zero-sized and out of flow, and its layer sizes to its content, which rules it out for a scrim. Its own style styles the layer. It hears only click, mouseenter, mouseleave and filedrop, so onmousedownoutside goes on a child.

Two native quirks, as of 0.10:

Its props:

prop values default
position { x, y } in window coordinates; overrides the trigger —
side top right bottom left — the trigger edge to hang off bottom
align start center end along that side start
anchor topLeft topCenter topRight rightCenter bottomRight bottomCenter bottomLeft leftCenter; derived from side + align when unset —
gap px between trigger and child 0
offset { x, y } px, applied after positioning { x: 0, y: 0 }
fit snap keeps it inside the window; switch flips the anchor snap
snapMargin px kept from the window edge when snapping 8
deferred paint in a later layer, above everything drawn so far true
priority order among deferred layers; higher paints later 1
occlude block hit testing on what lies beneath true