npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/screen-orientation

v3.0.0

Published

Declarative Screen Orientation component for Web Components. Framework-agnostic screen.orientation monitor + lock/unlock commands via wc-bindable-protocol.

Readme

@wcstack/screen-orientation

🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.

@wcstack/screen-orientation is a headless Screen Orientation component for the wcstack ecosystem.

It is not a visual UI widget. It is an async primitive node that turns screen.orientation into reactive state, and exposes lock()/unlock() as declarative commands.

With @wcstack/state, <wcs-screen-orientation> can be bound directly through path contracts:

  • input surface: none — screen.orientation is a single global with nothing to configure
  • output state surface: type, angle, portrait, landscape, error, errorInfo

Why this exists — a monitor/command asymmetry unique in this batch

Unlike @wcstack/network (a pure monitor), this node is bidirectional: it monitors orientation and exposes lock()/unlock() commands. This produces a notable internal asymmetry:

  • Monitoring needs no _gen generation guard. Subscribing to screen.orientation's change event is fully synchronous — there is no asynchronous probe whose stale resolution could race a dispose() (same reasoning as @wcstack/network).
  • lock() does need one. It is asynchronous and in-flight; a stale lock() resolving after a newer lock()/unlock() call must not clobber the state that call already established. This guard is entirely independent of the monitoring path.

lock() is best-effort. This is not a desktop-vs-mobile split: most current browsers, desktop and mobile alike, reject a plain-tab lock() call unless the document is fullscreen or running as an installed PWA (Safari does not support lock() at all, in any context). The rejection's error name varies by browser and cause — NotAllowedError (current spec, fullscreen pre-lock condition unmet), NotSupportedError (locking to that orientation unsupported), or SecurityError (older implementations) — so do not branch on a specific name. Never-throw: failures land in error, not as a rejected promise from the caller's perspective.

Install

npm install @wcstack/screen-orientation

Quick Start

1. Read live orientation

<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/screen-orientation/auto"></script>

<wcs-state>
  <script type="module">
    export default {
      portrait: true,
    };
  </script>
</wcs-state>

<wcs-screen-orientation data-wcs="portrait: portrait"></wcs-screen-orientation>
<template data-wcs="if: portrait|not">
  <p>Please rotate your device to portrait.</p>
</template>

One timing rule applies to this example: <wcs-screen-orientation> publishes its snapshot through wcs-orientation:change events, and the first snapshot fires synchronously at connect — before @wcstack/state has attached its binding listeners. The initial value still arrives, because every observable property on this node is output-only (declared in properties, absent from inputs): that makes the default binding authority element, so the binding reads the property directly when it attaches instead of waiting for an event it already missed (directional initial sync, on by default since v1.21.0). A device already in landscape at load time is reflected with no manual pull (see Notes & limitations).

2. Lock orientation on command

<wcs-screen-orientation data-wcs="command.lock: $command.lockLandscape; error: lockError"></wcs-screen-orientation>
<button data-wcs="onclick: lockLandscape">Lock to landscape</button>
export default {
  lockError: null,
  $commandTokens: ["lockLandscape"],
  lockLandscape() {
    this.$command.lockLandscape.emit("landscape");
  },
};

In a plain tab like this, clicking the button will not actually lock anything: current browsers reject lock() (surfacing in lockError) unless the document is fullscreen or running as an installed PWA. To see the lock take hold, pair this with a fullscreen trigger — e.g. <wcs-fullscreen> — and call lock() after entering fullscreen (see Notes & limitations).

Observable Properties (outputs)

| Property | Event | Description | | ----------- | ---------------------- | ------------ | | type | wcs-orientation:change | screen.orientation.type (e.g. "portrait-primary"), or null when unsupported. | | angle | wcs-orientation:change | screen.orientation.angle, or null when unsupported. | | portrait | wcs-orientation:change | true when type starts with "portrait". | | landscape | wcs-orientation:change | true when type starts with "landscape". | | error | wcs-orientation:error | The last lock()/unlock() failure, or null. | | errorInfo | wcs-orientation:error-info-changed | Serializable failure taxonomy (WcsIoErrorInfo: stable code / phase / recoverable) derived from error, or null. Additive — the error shape is unchanged. |

type/angle/portrait/landscape all derive from the single wcs-orientation:change event.

Commands

| Command | Async | Description | | -------- | ----- | ------------ | | lock | yes | Request a specific orientation lock (e.g. "landscape", "portrait-primary"). Value passed through verbatim — never-throw; an unrecognized string or unsupported environment surfaces via error. | | unlock | no | Release a previously requested lock. Synchronous, mirroring the platform API. |

Attributes / Inputs

None. screen.orientation is a single global; there is nothing per-instance to configure.

Notes & limitations

  • No secure-context requirement for monitoring (unlike @wcstack/geolocation/@wcstack/permission).
  • lock() needs a fullscreen or installed-PWA context — not a desktop-vs-mobile split. A plain-tab call typically rejects on both desktop and mobile (as NotAllowedError / NotSupportedError / SecurityError depending on browser and cause — do not branch on the name); Safari does not implement lock() at all. Design any UI around it being best-effort, and pair it with an explicit fullscreen entry point (e.g. @wcstack/fullscreen) when the lock actually needs to take hold.
  • The initial snapshot event misses bindings, but the value still arrives. The first wcs-orientation:change fires synchronously during connectedCallback — before @wcstack/state attaches its binding listeners (binding setup is deferred to a later microtask; see docs/timing-and-firing-contract.md §4.1) — and events are not replayed to late subscribers. The value is not lost, because every observable property here is output-only (properties only, never inputs), which makes the default binding authority element: the binding reads the property directly when it attaches (directional initial sync, on by default since v1.21.0). So portrait/landscape/type/angle are correct on first paint with no manual pull, and this holds for every monitor node. Only if you explicitly set enableDirectionalInitialSync: false do you need the older $connectedCallback + whenDefined pull again. See docs/timing-and-firing-contract.md §7 for the full firing/generation contract (initial snapshot, lock() generation ordering, error dedup).
  • errorInfo taxonomy (additive). Alongside error, <wcs-screen-orientation> publishes a serializable errorInfo (wcs-orientation:error-info-changed — note the wcs-orientation: namespace, not the tag name) that classifies the same lock()/unlock() failure into a stable WcsIoErrorInfo (code / phase / recoverable), without changing the error shape. A missing screen.orientation / method (synthetic "unsupported") → capability-missing (phase probe); the plain-tab lock rejections NotAllowedError / NotSupportedError / SecurityError all fold to a single not-allowed (phase execute, recoverable: false — matching the "don't branch on the name" model above); an AbortError (superseded by a newer lock()) → aborted (phase execute, recoverable: true — a fresh lock() may still succeed); anything else (e.g. InvalidStateError, a raw throw, a missing .name) → orientation-error (phase execute). errorInfo transitions exactly when error does (cleared to null on recovery); the shared WcsIoErrorInfo type and the WCS_SCREEN_ORIENTATION_ERROR_CODE constants are exported.
  • SSR (@wcstack/server). Declares static hasConnectedCallbackPromise = true; since monitoring is synchronous, connectedCallbackPromise always settles immediately.

CSS styling with :state()

<wcs-screen-orientation> reflects 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 | |-------|---------| | portrait | wcs-orientation:change fires with a type that starts with "portrait" | | landscape | wcs-orientation:change fires with a type that starts with "landscape" | | error | wcs-orientation:error fires with a non-null detail (cleared on null) |

portrait and landscape are mutually exclusive and both fall off when type is null (unsupported environment). Note the event namespace is wcs-orientation:, not the tag name wcs-screen-orientation; angle is not reflected (continuous value, excluded by design — see docs/custom-state-reflection-design.md §3.2).

wcs-screen-orientation:state(portrait) ~ .portrait-hint  { display: block; }
wcs-screen-orientation:state(landscape) ~ .landscape-hint { display: block; }

form:has(wcs-screen-orientation: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-screen-orientation> 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-screen-orientation: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. ["portrait"]). It is not part of wc-bindable (not a bind target) and its shape is not a guaranteed contract — use it for debugging only.

  • The debug-states attribute (opt-in, default off) mirrors state changes onto data-wcs-state-portrait / data-wcs-state-landscape / data-wcs-state-error attributes on the element, so the Elements panel highlights them as they toggle:

    <wcs-screen-orientation debug-states></wcs-screen-orientation>

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.

Headless usage (ScreenOrientationCore)

import { ScreenOrientationCore } from "@wcstack/screen-orientation";

const core = new ScreenOrientationCore();
core.addEventListener("wcs-orientation:change", (e) => {
  console.log((e as CustomEvent).detail); // { type, angle }
});

core.observe();
await core.lock("landscape");
console.log(core.error);

// later:
core.dispose();

The structural Core surface is normative across wcstack IO nodes (async-io-node-guidelines §3.9); to bind it into signals with no element at all, see @wcstack/signals — Binding a Core directly.

Accessibility

WCAG 1.3.4 Orientation (AA): content must not restrict itself to a single display orientation unless that orientation is essential (a bank-check scanner, a piano keyboard). A user with the device mounted on a wheelchair cannot rotate it. This README is precise about when lock() fails — the same precision applies to when it should be called: lock only when the orientation is essential to the task, and give the user a visible way back (a control wired to the unlock command). Monitoring (type / angle / portrait / landscape) carries no such obligation — adapting your layout to the reported orientation is exactly what 1.3.4 wants.

License

MIT