APERTURE / THE FIELD GUIDE

From first reveal
to your own world.

Aperture is a reveal primitive. It clips real DOM content using animated SVG geometry, with a native animation driver or optional GSAP adapter.

Start with a small, working world.

Three independent projects, each with source, a README and the local library included. Unzip, install, and make it yours. Node 22.12 or newer.

01 / VANILLA TYPESCRIPT

Four small entrances.

Modal, gallery, product details and in-page navigation. Keyboard and no-JS fallback patterns.

Download vanilla starter

02 / REACT

Your component. In motion.

A native dialog with the React hook, live settings, Strict Mode and automatic cleanup.

Download React starter

03 / NEXT.JS

Into the next chapter.

Server-rendered pages, a small client boundary, and a reveal before real route navigation.

Download Next.js starter
cd aperture-react-starter  # or vanilla / next
npm install
npm run dev

01. Install the local package

Version 0.1.0 is available as a download. An npm registry release will follow later.

Download the package
npm install ./bolted-aperture-0.1.0.tgz

02. Give your content a home

The content must be a direct child of the container. Set the container's dimensions and keep its padding at zero. Controls that open or close the scene should sit outside the content.

<div class="frame">
  <div class="scene">Your content here.</div>
</div>
<button id="reveal">Open / close</button>

<style>
.frame { position: relative; height: 480px; overflow: hidden; }
.scene { background: #d9e2fa; }
</style>

03. Make an entrance

import { createAperture } from '@bolted/aperture';

const reveal = createAperture({
  host: document.querySelector('.frame'),
  content: document.querySelector('.scene'),
  preset: 'folded-iris',
  duration: 1.1,
  origin: { x: 0.5, y: 0.5 },
  colors: ['#2546f0', '#ff6b35'],
});

document.querySelector('#reveal').addEventListener('click', () => {
  reveal.toggle();
});
// When your component unmounts: reveal.destroy();

04. Keep control

Method Behavior
open(origin?) Opens from normalized coordinates or an element's center.
close() / toggle() Interrupts smoothly, continuing from the current progress.
seek(progress) Stops playback and draws any point from 0 to 1.
setOptions(options) Updates geometry immediately; timing changes affect the next animation.
destroy() Removes owned markup, listeners and observers, and restores original inline styles.

Animation promises resolve with { status, progress }. Status is completed, interrupted, or destroyed. A new animation request resolves the previous one as interrupted.

05. Choose your feel

Option Values
preset folded-iris · memory-portal · liquid-lens · record-bloom · editorial-fold
duration Seconds. Default 1.1; close takes 80%; partial travel scales with distance.
easing smooth · gentle · snappy
spread / rotation Fold separation from 0 to 3; additional starting angle in degrees. Circles stay circular.
origin { x, y } in normalized 0–1 coordinates, or an HTMLElement.
colors Two SVG fill colors: outer edge, inner edge.
reducedMotion system (default), or always. The system preference cannot be overridden.

06. Plug into GSAP

npm install gsap

import { gsapDriver } from '@bolted/aperture/gsap';
const reveal = createAperture({ host, content, driver: gsapDriver });

The core does not import GSAP. The optional adapter uses GSAP to animate the same progress value, and kills its own tween when interrupted or destroyed.

07. Own the interaction

Aperture hides closed or transitioning content from keyboard interaction and assistive technology. Your app remains responsible for focus, Escape handling, scroll locking, and the meaning of the revealed content. Use native dialogs for modal experiences, and keep a close button available outside the reveal.

Reduced motion settles automatic transitions instantly. Manual scrubbing remains available. Every instance has unique SVG IDs and its own resize observer. Importing during server rendering is safe; create the controller only after mounting.

08. Bring it into React

"use client"; // Next.js interactive boundary
import { useAperture } from '@bolted/aperture/react';

export function Scene() {
  const { hostRef, contentRef, aperture } = useAperture({
    preset: 'folded-iris', duration: 1.1,
  });
  return <>
    <div ref={hostRef} style={{ height: 480, overflow: 'hidden' }}>
      <div ref={contentRef}>Your content.</div>
    </div>
    <button disabled={!aperture} onClick={() => aperture?.toggle()}>
      Open / close
    </button>
  </>;
}

React is an optional peer. The core does not import it. The hook accepts the core options, handles refs, option updates and cleanup, and exposes the same controller once both elements mount. Conditional mounting and Strict Mode are supported. Animation frames do not trigger component renders.

Use immutable settings. Supplied options patch the controller; omitted settings keep their previous values. Pass explicit values to reset them. Equivalent inline colors and origin coordinates do not redraw; callbacks use the latest committed props. Keep custom driver identity stable. Changing the driver or DOM elements creates a fresh, closed controller.

The React entry point imports safely during server rendering. In Next.js, put your interactive component behind a "use client" boundary. Keep ordinary links as fallbacks, and leave routing to your application. The Next.js starter demonstrates that pattern. For Vue, use the core in onMounted and clean up in onUnmounted.

09. Keep the content working

import { getApertureSupport } from '@bolted/aperture';
const { animated, missing } = getApertureSupport();
// Controller mode: 'animated' or 'instant'

Animation requires SVG clipping, native inert, ResizeObserver, animation frames and motion preference detection. Missing capabilities produce instant reveals with the same open/close API. No animation driver or SVG is used in that mode, and closed content stays hidden from interaction. Instant seek(p) shows content for positive values and hides it otherwise.

The capability check does not certify rendering. The package uses modern ESM and ES2022 syntax. It cannot provide a fallback in browsers unable to load that code. Physical iPhone 12 Safari testing and a wider browser matrix are still pending.

mode: 'animated' means capabilities are present. Automatic playback still respects system Reduce Motion and the always option. Your app owns focus, Escape, scroll locking and no-JS content; the starters include working patterns and native-dialog fallbacks.

10. Bring your assistant along

Choose a transition in the workbench and select Use with AI. Tell us your framework and intended interaction. Copy or download a Markdown brief with the exact recipe, matching setup example, package version and verification steps.

The brief includes the full JSON because assistants fetching a recipe link cannot see its URL fragment. You choose where to paste it. No assistant account or integration is required.

For tools that read documentation directly, llms.txt indexes the Markdown guide, effect catalog and recipe v1 schema. These are also included in the package. This is a discovery convention; each assistant decides what it reads.

import { parseRecipe, recipeToOptions } from '@bolted/aperture/recipes';
const recipe = parseRecipe(jsonText, { strict: true });
const options = recipeToOptions(recipe);
// createAperture({ host, content, ...options });
// useAperture(options);

Strict mode rejects unknown fields. Default imports preserve the existing behavior of dropping them. Recipe bounds match the workbench; the core API permits a wider spread range. The recipe format version and library version are separate. Aperture remains experimental and unpublished; use the matching local archive or starter.

Your next step

Explore a preset, tune it, and copy the live configuration.

Back to the workbench