Blackout UI

A darkness interaction engine for the web

Darkness outside.
Your website inside.

Blackout UI places a fullscreen black layer over your existing page and reveals the page — exactly as it already renders — around the pointer, like a flashlight. It does not repaint your site, convert it to dark mode, or touch a single line of your CSS.

Click to activate the flashlight.

Live playground

Configure the real instance

This isn't a mockup — these controls call the actual blackout.configure() API on the same instance running the hero above. Turn Blackout on to see changes as you make them.

Shape
Native cursor
Touch input
On pointer leave

How it works

Blackout UI does not redesign your website

It adds one fullscreen, non-interactive layer above the page and reveals the existing rendered content around the pointer. Nothing about your page changes underneath it.

Not another dark mode library

It doesn't restyle anything

Traditional dark-mode libraries rewrite your page: backgrounds go black, text goes white, images get adjusted, components get restyled. Blackout UI does something fundamentally different.

Typical dark-mode library

  • background → black
  • text → white
  • images → adjusted
  • components → restyled

Blackout UI

  • Your website stays exactly as it is
  • A black layer goes on top
  • The pointer reveals the website underneath

Features

What's actually implemented

Zero dependencies

The published package ships with an empty dependencies field. Nothing to audit, nothing to update.

Framework agnostic

Plain JavaScript, TypeScript, React, Next.js, Vue, Svelte — it's just DOM and CSS underneath, no framework required.

Non-invasive

Blackout UI adds one overlay element and one scoped stylesheet. It never rewrites your existing HTML or CSS.

Theme independent

Works with light mode, dark mode, or a theme that changes at runtime — the reveal just shows whatever is currently rendered.

Touch support

Pointer Events unify mouse, pen, and touch, with a configurable touchBehavior for what happens on lift-off.

SSR safe

Every method is a documented no-op when window/document don't exist — safe to import on the server.

Accessible by default

The overlay is aria-hidden, non-focusable, and pointer-events: none by default — nothing underneath is blocked.

Lightweight

Loading current build size…

Configuration

Every public option

Passed to Blackout.init(options) or blackout.configure(options). Every field is optional; invalid values are clamped to a safe default rather than thrown.

enabledboolean

Whether the effect is active immediately after init().

Default: true

Blackout.init({ enabled: false });
radiusnumber

Outer radius of the flashlight, in CSS pixels.

Default: 180

blackout.configure({ radius: 250 });
softnessnumber (0–100)

How gradually the flashlight fades to black. 0 is a hard edge, 100 fades from the very center.

Default: 35

blackout.configure({ softness: 60 });
darknessnumber (0–1)

Opacity of the black outside the flashlight. 1 is fully opaque black.

Default: 1

blackout.configure({ darkness: 0.9 });
intensitynumber (0–1)

How fully the page is revealed inside the flashlight. 1 is a full, undimmed reveal.

Default: 1

blackout.configure({ intensity: 0.8 });
shape"circle" | "ellipse"

Shape of the reveal area.

Default: "circle"

blackout.configure({ shape: 'ellipse' });
touchboolean

Whether touch input moves the flashlight.

Default: true

blackout.configure({ touch: false });
touchBehavior"follow" | "hide" | "persist"

What happens to the flashlight when an active touch lifts off. "follow" keeps it in place, "hide" returns to full darkness, "persist" is an explicit alias for "follow".

Default: "follow"

blackout.configure({ touchBehavior: 'hide' });
onPointerLeave"freeze" | "center" | "hide"

What happens when the pointer leaves the viewport entirely.

Default: "freeze"

blackout.configure({ onPointerLeave: 'center' });
initialPosition"center" | object

Where the flashlight sits before any pointer input has been received. Accepts "center" or an explicit { x, y } point.

Default: "center"

Blackout.init({ initialPosition: { x: 100, y: 100 } });
cursor.hideboolean

Hides the native cursor while the effect is enabled. Fully restored on disable()/destroy().

Default: false

blackout.configure({ cursor: { hide: true } });
accessibility.respectReducedMotionboolean

Reserved for future animated transitions. Pointer tracking itself is never animated regardless of this setting.

Default: true

Blackout.init({ accessibility: { respectReducedMotion: true } });
zIndexnumber

z-index of the overlay element.

Default: 2147483647

Blackout.init({ zIndex: 999999 });

API reference

Every public method

Blackout.init(options?)

Does: Mounts the overlay and starts pointer tracking. Idempotent — a second call updates configuration instead of creating a second overlay.

Parameters: optional BlackoutOptions.

Returns: the BlackoutInstance (itself), so you can chain .enable().

Use when: once, on page/component mount.

const blackout = Blackout.init({ radius: 200 });
blackout.configure(options)

Does: Updates configuration on the existing instance without recreating the overlay.

Parameters: a partial BlackoutOptions.

Returns: the instance.

Use when: live-updating settings, e.g. from UI controls (see the playground above).

blackout.configure({ radius: 300, softness: 60 });
blackout.enable() / blackout.disable()

Does: Turns the effect on/off without destroying the instance or its configuration.

Parameters: none.

Returns: the instance.

Use when: responding to a user action, like this page's "Turn On Blackout" button.

blackout.enable();
blackout.disable();
blackout.toggle()

Does: Flips the current enabled state.

Parameters: none.

Returns: the instance.

Use when: a single button should switch between on/off.

blackout.toggle();
blackout.destroy()

Does: Fully removes the overlay, listeners, and injected styles; cancels any pending frame; restores the cursor. Safe to init() again afterward.

Parameters: none.

Returns: void.

Use when: unmounting a component (see the React/Vue/Svelte examples below).

blackout.destroy();
blackout.isEnabled() / blackout.isInitialized()

Does: State getters.

Parameters: none.

Returns: boolean.

Use when: syncing your own UI (e.g. a status chip) with the instance's state.

if (blackout.isEnabled()) { /* ... */ }
blackout.setPosition(x, y)

Does: Manually moves the flashlight to a viewport coordinate.

Parameters: x: number, y: number (CSS pixels, viewport-relative).

Returns: the instance.

Use when: scripting the reveal instead of following the pointer.

blackout.setPosition(400, 250);
blackout.setRadius(n) / setSoftness(n) / setDarkness(n)

Does: Shorthand for configure({ radius: n }), etc.

Parameters: n: number.

Returns: the instance.

Use when: updating a single value.

blackout.setRadius(220);
blackout.getOptions()

Does: Returns the current, fully-resolved configuration.

Parameters: none.

Returns: ResolvedBlackoutOptions.

Use when: reading back the current radius/softness/etc. — this is exactly how the playground's sliders stay in sync.

const { radius } = blackout.getOptions();
blackout.on(event, fn) / blackout.off(event, fn)

Does: Subscribes/unsubscribes to "enable", "disable", "configure", "destroy".

Parameters: event name, listener function.

Returns: the instance (for on/off).

Use when: syncing external UI state to instance lifecycle events.

blackout.on('enable', () => console.log('on'));
createBlackout()

Does: Creates an independent Blackout instance, separate from the default singleton.

Parameters: none.

Returns: a new BlackoutInstance.

Use when: an app genuinely needs more than one instance.

import { createBlackout } from 'blackout-ui';
const secondary = createBlackout();

Install

Basic usage

npm install blackout-ui
import { Blackout } from "blackout-ui";

const blackout = Blackout.init();
blackout.enable();

Blackout is also available as the module's default export (import Blackout from "blackout-ui") — both refer to the same singleton instance.

Full configuration example

import { Blackout } from "blackout-ui";

const blackout = Blackout.init({
  radius: 220,
  softness: 45,
  darkness: 0.95,
  intensity: 1,
  shape: "circle",
  touch: true,
  touchBehavior: "follow",
  onPointerLeave: "freeze",
  cursor: { hide: false },
});

// Nothing is visible until you explicitly enable it:
blackout.enable();

CDN / no build step

<script src="https://unpkg.com/blackout-ui"></script>
<script>
  const blackout = BlackoutUI.init();
  blackout.enable();
</script>

Module formats

Framework examples

Where the moving parts go

The full examples live in the repository under examples/. Every one follows the same rule: call init()/enable() only where code is guaranteed to run in the browser, and call destroy() on unmount.

Vanilla JS

No build step required. Import directly, or use the CDN global build.

import Blackout from 'blackout-ui';

const blackout = Blackout.init({ radius: 200, softness: 40 });
document.getElementById('toggle')
  .addEventListener('click', () => blackout.toggle());

React

Mount in useEffect; tear down in its cleanup function. Idempotent init()/reversible destroy() makes this safe under Strict Mode's double-invoke.

useEffect(() => {
  const blackout = Blackout.init(options);
  return () => blackout.destroy();
}, []);

Next.js (App Router)

Blackout UI's methods no-op on the server, so importing it is safe anywhere. Actually calling init() needs a 'use client' component mounted via useEffect.

'use client';
useEffect(() => {
  const blackout = Blackout.init(options);
  return () => blackout.destroy();
}, []);

Vue / Nuxt

Use onMounted/onUnmounted. In Nuxt, this already only runs client-side.

onMounted(() => Blackout.init());
onUnmounted(() => Blackout.destroy());

Svelte / SvelteKit

Use onMount/onDestroy. In SvelteKit, onMount only ever runs in the browser.

onMount(() => Blackout.init());
onDestroy(() => Blackout.destroy());

Use cases

A visual interaction primitive, not a theme

Blackout UI is a building block for interaction, not a replacement for your site's styling. A few places it fits:

Interactive portfolios

A dramatic, deliberate reveal for case studies or hero imagery.

Landing pages

Turn a static product screenshot into something the visitor has to explore.

Creative / experimental sites

Use the flashlight as a first-class part of the visual identity.

Games and experiments

Darkness-and-reveal as an interaction mechanic, not just decoration.

Events / launches

A single moment of "unveiling" content without building a custom effect from scratch.

Accessibility

What's actually implemented

Behavior & edge cases

Honest answers, not assumptions

SituationBehavior
Light/dark theme switchingNo special handling needed — the flashlight reveals whatever is currently rendered, live.
Fixed / sticky elementsRendered normally underneath; the overlay is position: fixed to the viewport and repaints independently of page scroll.
Modals, images, video, canvasRevealed as-is through the flashlight. Blackout UI never touches their rendering.
Responsive layouts / resizeThe overlay listens for resize and repaints; it never assumes a fixed viewport size.
Touch devicesPointer Events cover touch input directly; see the touch / touchBehavior options above.
SPA navigationThe overlay is independent of any specific page element, so client-side route changes don't require re-initialization.
SSRImporting the package and calling any method is a documented no-op on the server — see the SSR-safety notes above.
Same-origin iframesVisually covered by the overlay, since it sits above the whole viewport.
Cross-origin iframesLimitation: the overlay cannot reach into a cross-origin iframe's own document — browsers don't allow that, by design. It still visually covers the iframe from the parent page's viewport.

FAQ

Common questions

Does Blackout UI change my website's colors?

No. It overlays the page with a black layer and cuts a reveal hole around the pointer — it never converts your page into another theme or rewrites its CSS.

Does it work with dark mode?

Yes. It reveals whatever theme is currently rendered underneath, with no theme detection required.

Does it modify my DOM?

It adds exactly one overlay <div> and one <style> tag. It never modifies, clones, or restyles your existing elements.

Does it block clicks?

No, by default the overlay is pointer-events: none. Clicks, hovers, and drag interactions pass straight through to your page.

Does it work on mobile?

Yes — touch input is handled through the Pointer Events API. See the touch and touchBehavior options.

Does it work with React/Next.js?

Yes — see the framework examples above. Call init() in useEffect and destroy() in its cleanup.

Is it SSR safe?

Yes. Every public method checks for a browser environment first and is a documented no-op on the server.

Does it require a framework?

No. The core package has no framework dependency and works with plain JavaScript.

Does it have dependencies?

No runtime dependencies — the published package's dependencies field is empty.

Can I turn it on/off dynamically?

Yes — blackout.enable(), blackout.disable(), and blackout.toggle(), exactly like the button at the top of this page.