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

universal-readonly

v0.1.0

Published

Universal readonly / interaction-control layer for web applications (framework-agnostic).

Readme

universal-readonly

Small, framework-agnostic DOM interaction guard for putting a page or a selected section into read-only mode.

Install

npm install universal-readonly

Usage

import { readonlyMode } from 'universal-readonly';

readonlyMode.enable({ selector: '#order-form' });

// Calling enable again replaces the active configuration.
readonlyMode.enable({ allow: { links: false, contextMenu: false } });

readonlyMode.disable();
readonlyMode.toggle();
console.log(readonlyMode.isEnabled());

The module does not access the DOM when imported, so it can be imported by an SSR build. Call enable() from browser-only code after the document is available.

Options

interface ReadonlyOptions {
  selector?: string | string[];
  allow?: {
    scroll?: boolean;
    selection?: boolean;
    copy?: boolean;
    links?: boolean;
    download?: boolean;
    focus?: boolean;
    contextMenu?: boolean;
    keyboard?: boolean;
  };
}

selector accepts a CSS selector, a comma-separated selector string, or an array of selectors. Without a selector, the entire document is guarded. Invalid selectors throw a SyntaxError before the mode is enabled.

All permissions default to true except keyboard, which defaults to false:

| Permission | Allows | | --- | --- | | scroll | Wheel and touch scrolling | | selection | Text selection | | copy | Copy, cut, paste, and drop | | links | Normal anchor navigation | | download | Anchors with a download attribute | | focus | Pointer focus on controls and links | | contextMenu | The context menu | | keyboard | Keyboard editing and input/change events |

Form submission and button/control clicks are always blocked while the mode is active. Events outside the selected roots are unaffected. Event listeners use capture phase so the guard runs before handlers on descendants. Selector scopes automatically update when matching elements are added or removed.

Strict mode

readonlyMode.enable({
  allow: {
    scroll: false,
    selection: false,
    copy: false,
    links: false,
    download: false,
    focus: false,
    contextMenu: false,
    keyboard: false
  }
});

Platform support

universal-readonly is a client-side browser library. It works by registering standard DOM event listeners on document, so it can protect a full page or selected sections of the document. Selector scopes continue tracking matching elements added or removed while the mode is enabled.

| Platform | Support | Notes | | --- | --- | --- | | Chrome and Chromium | Supported | Desktop, Android, and Chromium-based WebViews | | Microsoft Edge | Supported | Chromium-based Edge | | Firefox | Supported | Desktop and Android | | Safari | Supported | macOS and iOS/iPadOS | | Plain JavaScript or TypeScript | Supported | No framework or runtime dependency | | React, Vue, Angular, Svelte | Supported | Enable and disable it from the component lifecycle | | Next.js, Nuxt, SvelteKit, Remix | Supported | Import is SSR-safe; call enable() only on the client | | Electron renderer and hybrid WebViews | Supported | Requires a normal DOM document | | Node.js | Tooling and import only | Node does not provide the DOM events required by enable() |

The package targets modern browsers with support for addEventListener, capture-phase events, CSS selectors, and standard Element and Node APIs. It is published as an ES module and requires Node.js 18 or newer for package tooling.

Framework and CDN integration

The package is framework-agnostic and has no Angular, React, Vue, or Svelte dependency. Install it once with npm, then import it from the component that owns the read-only state:

npm install universal-readonly

Use the framework's client-side mount and cleanup lifecycle so listeners do not remain active after a view is removed.

For a normal <script> tag, load the browser bundle before your application script:

<form id="order-form">
  <input name="name" />
  <button type="submit">Submit</button>
</form>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser.js"></script>
<script>
  window.readonlyMode.enable({ selector: '#order-form' });

  // Later, when the section is removed:
  // window.readonlyMode.disable();
</script>

Use this alternative CDN URL if preferred:

<script src="https://unpkg.com/[email protected]/dist/browser.js"></script>

The browser bundle exposes window.readonlyMode and does not require Angular, React, Vue, Svelte, or any other framework.

For applications using JavaScript modules, load the smaller module build:

<script type="module">
  import readonlyMode from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js';

  readonlyMode.enable({ selector: '#order-form' });
</script>

The equivalent unpkg import is:

import { readonlyMode } from 'https://unpkg.com/[email protected]/dist/index.js';
import { useEffect } from 'react';
import { readonlyMode } from 'universal-readonly';

export function ReadonlyOrder({ locked }: { locked: boolean }) {
  useEffect(() => {
    if (!locked) return;

    readonlyMode.enable({ selector: '#order-form' });
    return () => readonlyMode.disable();
  }, [locked]);

  return <form id="order-form">{/* form fields */}</form>;
}
import { AfterViewInit, Component, OnDestroy } from '@angular/core';
import { readonlyMode } from 'universal-readonly';

@Component({
  selector: 'app-order-form',
  template: `
    <form id="order-form">
      <input name="name" />
      <button type="submit">Submit</button>
    </form>
  `
})
export class OrderFormComponent implements AfterViewInit, OnDestroy {
  ngAfterViewInit(): void {
    readonlyMode.enable({ selector: '#order-form' });
  }

  ngOnDestroy(): void {
    readonlyMode.disable();
  }
}
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue';
import { readonlyMode } from 'universal-readonly';

onMounted(() => {
  readonlyMode.enable({ selector: '#order-form' });
});

onBeforeUnmount(() => {
  readonlyMode.disable();
});
</script>

<template>
  <form id="order-form">
    <input name="name" />
    <button type="submit">Submit</button>
  </form>
</template>
<script lang="ts">
  import { onMount } from 'svelte';
  import { readonlyMode } from 'universal-readonly';

  onMount(() => {
    readonlyMode.enable({ selector: '#order-form' });
    return () => readonlyMode.disable();
  });
</script>

<form id="order-form">
  <input name="name" />
  <button type="submit">Submit</button>
</form>

Add 'use client'; to a client component and enable the mode in useEffect:

'use client';

import { useEffect } from 'react';
import { readonlyMode } from 'universal-readonly';

export function ReadonlySection() {
  useEffect(() => {
    readonlyMode.enable({ selector: '#order-form' });
    return () => readonlyMode.disable();
  }, []);

  return <form id="order-form">{/* form fields */}</form>;
}

Use onMounted in a component, or place the integration in a client-only component:

<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue';
import { readonlyMode } from 'universal-readonly';

onMounted(() => readonlyMode.enable({ selector: '#order-form' }));
onBeforeUnmount(() => readonlyMode.disable());
</script>

Use the equivalent browser-only component lifecycle. No framework adapter or additional package is required. Call readonlyMode.enable() after the client view mounts and readonlyMode.disable() when it is destroyed.

SSR and server environments

Importing the package is safe during server rendering because it does not access window or document at module load time. enable() and selector resolution require a browser document, so do not call them from server-only code.

For an SSR application, place the call in a client-only lifecycle hook. For example, use React useEffect, Vue onMounted, Angular ngAfterViewInit, or Svelte onMount. Always clean up with readonlyMode.disable() when the view is destroyed.

Limitations

  • This package controls user DOM events; it is not a security boundary.
  • JavaScript can still change values, submit requests, or mutate application state directly.
  • It does not replace server-side authorization or API validation.
  • It does not automatically add readonly, aria-readonly, or disabled attributes to form controls.
  • Selector scopes are kept up to date when matching elements are added or removed while the mode is enabled.
  • Composed event paths are supported for events crossing open shadow-DOM boundaries; closed shadow roots may limit what the browser exposes to document listeners.
  • Form submissions and button/control clicks are always blocked while enabled.
  • The keyboard permission controls keyboard editing and input/change events; clipboard actions are controlled separately by copy.
  • Browser extensions, automation tools, and assistive technology may not behave like ordinary pointer and keyboard input.

Development

npm test
npm run build

npm publish runs a clean build and the test suite through prepublishOnly.

License

MIT