@wcstack/share
v1.19.1
Published
Declarative Web Share component for Web Components. Framework-agnostic navigator.share primitive via wc-bindable-protocol.
Downloads
275
Maintainers
Readme
@wcstack/share
@wcstack/share is a headless Web Share component for the wcstack ecosystem.
It is not a visual UI widget.
It is a command-only async primitive node that turns navigator.share(data) — click, native share sheet, resolve/reject — into a single declarative command, the same way @wcstack/notification turns Notification into reactive state plus a notify command.
With @wcstack/state, <wcs-share> can be bound directly through path contracts:
- command surface:
share(data)— a single async command, invoked ascommand.share: $command.doShare - output state surface:
value,loading,error,cancelled
This means a "Share this article" button can be expressed declaratively in HTML — success, failure, and the user simply dismissing the native share sheet are three distinct, bindable outcomes — without writing navigator.share() / try/catch glue in your UI layer.
@wcstack/share follows the wcstack Core/Shell architecture:
- Core (
ShareCore) wrapsnavigator.share(data)behind a single_gengeneration guard, same-value-guardedloading/error/cancelledsetters (valueis exempt — a completion signal that fires on every success), and a never-throwtry/catch - Shell (
<wcs-share>) connects that command to DOM lifecycle and exposescanShare(data)as a plain synchronous method - Binding Contract (
static wcBindable) declares observablepropertiesand a singlesharecommand (deliberately noinputs, noabortcommand)
Why this exists — a command-only node, and cancellation is not an error
Every other wcstack IO node either monitors a continuous state (network, permission) or configures something ahead of time and observes it change (fetch's url, geolocation's enableHighAccuracy). navigator.share() is different: it is a one-shot "call → native share sheet → resolve/reject" action with no continuous state to configure or watch, and (unlike fetch) no way to abort an in-flight call — there is no AbortSignal option, and the platform allows only one modal share sheet at a time, so the "a new call supersedes the previous one" plumbing fetch needs has no counterpart here.
The other defining decision is separating cancelled from error. When a user simply closes the native share sheet, navigator.share() rejects with an AbortError — exactly like closing a <dialog>. Folding that into error would make a binding gated on error (e.g. showing a "sharing failed" banner only when it is set) also fire on routine, harmless user cancellation, which is a UX bug waiting to happen. <wcs-share> keeps cancelled as its own boolean/event, so error reflects only genuine platform failures (NotAllowedError, TypeError, etc.).
See
docs/web-share-tag-design.mdfor the full design rationale.
Install
npm install @wcstack/shareQuick Start
1. Share an article, with cancellation handled separately from failure
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/share/auto"></script>
<wcs-state>
<script type="module">
export default {
$commandTokens: ["doShare"],
loading: false,
error: null,
cancelled: false,
onShareClick() {
this.$command.doShare.emit({
title: document.title,
url: location.href,
});
},
};
</script>
</wcs-state>
<wcs-share
data-wcs="command.share: $command.doShare; loading: loading; error: error; cancelled: cancelled"
></wcs-share>
<button data-wcs="onclick: onShareClick; disabled: loading">Share</button>
<template data-wcs="if: error">
<p>Sharing failed: <span data-wcs="textContent: error.message"></span></p>
</template>Because share() must run from within a real user gesture (a click handler), the button's click handler calls $command.doShare.emit(...) directly — <wcs-share> has no autoTrigger shortcut of its own (see Notes & limitations).
2. canShare(data) — checking feasibility ahead of time
<script type="module">
const shareEl = document.querySelector("wcs-share");
if (shareEl.canShare({ url: location.href })) {
// show the Share button
}
</script>Observable Properties (outputs)
| Property | Event | Description |
| ----------- | ------------------------------- | ------------ |
| value | wcs-share:complete | An echo of the data object passed to the share() call that just completed successfully, signalling "this share succeeded" (navigator.share() itself resolves Promise<void> with no payload). null before any successful share. |
| loading | wcs-share:loading-changed | true while a share() call is in flight. |
| error | wcs-share:error | A genuine platform failure (anything other than the user cancelling the share sheet). null when there has been no failure yet, or after the next share() call resets it. |
| cancelled | wcs-share:cancelled-changed | true when the user dismissed the native share sheet (AbortError). Kept independent of error so bindings gated on error do not react to routine cancellation. |
cancelled and error are both reset (false / null) at the start of a share() call that goes on to actually invoke navigator.share(), so a stale outcome from a previous call never lingers into that call's result. The one exception is the unsupported early-return (navigator.share missing, see below): it returns before that reset runs, so a cancelled left over from an earlier call can still read true alongside the freshly-set unsupported error. This is a narrow edge case in practice — a page losing navigator.share mid-session is not a realistic scenario.
Commands
| Command | Async | Description |
| ------- | ----- | ------------ |
| share | yes | Invokes navigator.share(data) with a single options object ({ title?, text?, url?, files? }) passed as one positional argument. |
There is no abort command — the Web Share API offers no mechanism to cancel an in-flight share() call from the caller's side.
canShare(data) — a plain synchronous method, not part of wcBindable
navigator.canShare(data) is a synchronous, side-effect-free predicate. It does not fit the wc-bindable properties shape (an observable with no arguments) or the commands shape (fire-and-observe-via-event); it is exposed directly as a plain instance method:
const canShare: boolean = shareEl.canShare({ url: "https://example.com" });It returns false (rather than throwing) when navigator.canShare is absent.
Attributes / Inputs
None. share(data)'s data varies on every call — it is a command argument, not a value to park on the element ahead of time as an attribute.
CSS styling with :state()
<wcs-share> reflects three boolean output states onto its
ElementInternals CustomStateSet,
so you can style it directly from CSS with the :state() pseudo-class — no
data-wcs binding or extra class toggling required.
| State | On when |
|-------|---------|
| loading | wcs-share:loading-changed fires with true (cleared on false) |
| cancelled | wcs-share:cancelled-changed fires with true (cleared on false) |
| error | wcs-share:error fires with a non-null detail (cleared on null) |
wcs-share:state(loading) ~ .spinner { display: block; }
wcs-share:state(loading) ~ .spinner { display: none; } /* default */
wcs-share:state(cancelled) ~ .hint { display: block; }
form:has(wcs-share:state(error)) .banner { display: block; }Unlike attributes or classes, :state() cannot be written from outside the
element, so there is no risk of confusing this output state with an input.
Browser support (:state(x) syntax): Chrome/Edge 125+, Safari 17.4+,
Firefox 126+. In older browsers the states are simply never set — :state()
selectors never match, but <wcs-share> itself keeps working normally
(graceful degradation, never-throw).
SSR: :state() cannot be serialized into HTML, so server-rendered markup
never carries these states on first paint (@wcstack/server is unaffected).
If you need to style the pre-hydration gap, pair your rule with
wcs-share:not(:defined) instead.
Debugging
Custom states are invisible in DevTools' Elements panel and attachInternals()
cannot be called twice, so there is no console way to inspect them directly.
Two debug-only aids are provided for that:
el.debugStates— a snapshot array of the currently-on state names (e.g.["loading"]). It is not part ofwc-bindable(not a bind target) and its shape is not a guaranteed contract — use it for debugging only.The
debug-statesattribute (opt-in, default off) mirrors state changes ontodata-wcs-state-loading/data-wcs-state-cancelled/data-wcs-state-errorattributes on the element, so the Elements panel highlights them as they toggle:<wcs-share debug-states></wcs-share>
Write your CSS against :state(), not data-wcs-state-*. The mirrored
attributes exist purely to make state changes visible while debugging with
DevTools open; they are not a supported styling hook.
Notes & limitations
- No
autoTrigger.navigator.share()must be invoked from within a real user gesture. A node-provided auto-trigger would not itself carry that gesture context, so — like@wcstack/fullscreen— this node has none. Wire the click handler directly to$command.doShare.emit(...). - No
abortcommand. There is no platform mechanism to cancel an in-flightnavigator.share()call. cancelledis independent oferror.AbortError(the user closed the share sheet) setscancelled, nevererror. Every other rejection setserror, nevercancelled.unsupportedhas no dedicated flag. Callingshare()whennavigator.shareis not a function immediately setserrorto{ message: "Web Share API is not supported in this browser." }and resolves withnull— no_genis consumed, since no asynchronous work is started. CheckcanShare, ortypeof navigator.share, ahead of time if you want to hide the UI proactively.- SSR (
@wcstack/server). Declaresstatic hasConnectedCallbackPromise = trueand exposesconnectedCallbackPromise; since there is no asynchronous probe, this promise always settles immediately (readyisPromise.resolve()). - Same-value guard applies to
loading/error/cancelled, but NOT tovalue. Those three setters only dispatch when the value actually changes (reference equality,===), since they are idempotent state.valueis different: it is a success-completion signal, andwcs-share:completeis the sole success notification, so it fires on every successfulshare()— with no same-value guard. This means a data-lessshare()(echoingnullwhenvalueis alreadynull) still dispatcheswcs-share:complete, and two consecutive successfulshare()calls passed the same object reference asdatadispatchwcs-share:completetwice (once per completion). This matches how@wcstack/clipboard(read) and@wcstack/broadcast(message) treat result/event values — a completion is an occurrence, not idempotent state.
Headless usage (ShareCore)
The Core has no DOM dependency and can be used directly:
import { ShareCore } from "@wcstack/share";
const share = new ShareCore();
share.addEventListener("wcs-share:complete", (e) => {
console.log((e as CustomEvent).detail.value); // the echoed data
});
share.addEventListener("wcs-share:cancelled-changed", (e) => {
console.log("cancelled:", (e as CustomEvent).detail);
});
await share.share({ title: "Article", url: location.href });
// later, when done:
share.dispose(); // invalidate any in-flight share() so a stale resolve is droppedLicense
MIT