Use this guide to make your app embeddable in Hazel as an ExoLivelit. Your app runs in an iframe and communicates with Hazel using postMessage. This is a minimal proof of concept implementation; we will be implementing more message types to expose more Hazel functionality, as well as support for incremental syntax updates when it becomes necessary.
This guide has been claude hardened; a claude was able to oneshot adapt an unrelated ts app to an exolivelit and make the relevant hazelside changes by being pointed at this file in an alongside clone of the hazel repo, given additionally a description of how the relevant part of that app's data model should be represented in the JSON format hazel can consume. You can check out the value builder exolivelit to familiarize yourself with the data schema supported.
- Iframe-compatible: Your app must work inside an iframe
- PostMessage communication: Follow Hazel's protocol for bidirectional messaging
- URL parameters: Read
idandparentOriginfrom query string - Auto-resize: Measure and report your content dimensions
- Value serialization: Accept initial state and send updated state as stringified JSON
Hazel loads your app with: https://your-app/?id=<uuid>&parentOrigin=<hazel-origin>
id: Unique identifier for this projector instanceparentOrigin: Hazel's origin forpostMessagetarget (security)
To start, it suffices to send ready and setSyntax, and to handle init. More of the internal livelits API will be exposed here in the future.
type ToHazelMessage =
| { type: "ready"; id: string } // Sent on mount
| { type: "setSyntax"; id: string; codec: string; value: string } // Send edits
| { type: "resize"; id: string; width: number; height: number }; // Report sizetype FromHazelMessage =
| { type: "init"; id: string; value: string } // Initial value
| {
type: "constraints";
id: string;
maxWidth: number;
maxHeight: number;
minWidth?: number;
minHeight?: number;
}; // Size limits- App mounts → sends
{type: 'ready', id} - Hazel responds →
{type: 'init', value: "..."}+{type: 'constraints', ...} - App applies constraints → sets
maxWidthCSS, starts auto-resize - User edits → app sends
{type: 'setSyntax', codec: 'json', value: JSON.stringify(newValue)} - Content grows → app sends
{type: 'resize', width, height}(debounced)
Copy these 3 files into your e.g. src/hooks/ directory:
src/
├── components/
│ └── MyEditor.tsx # Core app logic (Hazel-agnostic)
├── hooks/
│ ├── hazel-integration-base.ts # Shared base hook
│ ├── resize-strategies.ts # Resize implementations
│ └── useHazelIntegration.ts # App-specific wrapper
└── App.tsx # Entry point, uses Hazel integration
// App.tsx
import { useState } from "react";
import { useHazelIntegration } from "./useHazelIntegration";
export default function App() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id") || "local-demo";
const [value, setValue] = useState<any>(0);
const { setSyntax } = useHazelIntegration({
id,
codec: "json" /* The only codec supported for now */,
onInit: (valueStr) => {
setValue(JSON.parse(valueStr));
},
onConstraints: (c) => {
document.body.style.maxWidth = `${c.maxWidth}px`;
},
});
const handleChange = (newValue: any) => {
setSyntax(JSON.stringify(newValue));
};
return (
<div style={{}}>
<h3>My Hazel-Embedded App</h3>
{/* Your editor UI here */}
<button onClick={() => handleChange(value + 1)}>
Increment: {value}
</button>
</div>
);
}DO:
- Use flexible, responsive layouts (
flex,grid) - Allow content-driven height (
min-heightinstead of fixedheight) - Apply
maxWidthfrom constraints for responsive behavior
DON'T:
- Set fixed
height: 100vhon root containers - Use viewport units that ignore iframe constraints
- Create horizontal scrolling (respect
maxWidth)
We don't currently support dynamic registration for new kinds of exolivelit, although there is no particular blocker to doing so. Right now to add a new one, you'll need to clone the hazel repo and change two definitions in the Exo.re file.
Add YourApp to the Exo.kind type. This will determine the Hazel UI name of your exolivelit.
type kind =
| ...
| YourApp;Add a corresponding case to the Exo.module_of_kind function. This requires a prod and option dev URL for your app. The shape property determines how the text flow resumes to the right of your livelit; pick Block if in doubt. The rest of the properties (guard and size) are work-in-progress and will likely be set dynamically via content negoiation in the future; copying the values below should suffice for a prototype.
| YourApp => {
kind,
prod: "https://yourdomain.com", (* Your public URL for the app *)
dev: "http://localhost:port", (* Your internal dev path, if applicable *)
shape: Block, (* Block: After livelit, text flow continues from bottom line. Tab: Continues from top *)
guard: _ => true, (* Determines what Hazel syntax your app can be applied to; okay to leave as this for now *)
size: {
width: 680, (* init width in px; *)
height: 490, (* init height in px *)
},
}external-apps/simple-slider/: Integer sliderexternal-apps/value-builder/: Compositional Hazel value editorhttps://github.com/disconcision/nool/pull/4: A PR updating a math toy to support livelit embedding