# Nodality — full documentation Generated from 89 pages. Source: https://nodalityjs.github.io --- # Agents – getting started Path: /docs/agents/start # Agents – getting started Nodality is written by people and by coding agents. A page is two plain arrays – `elements` (what exists) and `nodes` (what is done to it) – and plain data is what a language model writes well and what a validator can check before anything renders. This section is for the agent that **writes** pages. For an agent that **operates** a finished page, see [Agent surface](../raster/agent-surface.md). ## One command ```bash npx nodality skill ``` It installs the `/nodality` skill – the authoring workflow, the node families, and the house rules the schema cannot encode – into Claude Code (`.claude/skills/`) and Cursor (`.cursor/rules/`), and registers the [MCP server](tools.md) in the project's config. | Flag | Effect | |---|---| | `--claude` | Claude Code only | | `--cursor` | Cursor only (per project) | | `--global` | Install into your home directory instead of the project | | `--dir=` | Copy `SKILL.md` for any other agent | | `--no-mcp` | Skip registering the MCP server | Re-run it after upgrading nodality to refresh the skill in place. ## The MCP server on its own ```json { "mcpServers": { "nodality": { "command": "npx", "args": ["nodality", "mcp"] } } } ``` ## What an agent has without a network `schema.json` (every element type and parameter, generated from the source) and `API.md` (an index of these docs) ship inside the npm package, so an agent reading `node_modules/nodality/` has the vocabulary to hand. [`llms.txt`](https://nodalityjs.github.io/llms.txt) and [`llms-full.txt`](https://nodalityjs.github.io/llms-full.txt) index the site. ## The loop 1. Read the vocabulary: `list_ops` for nodes, `get_schema` for the element type you are about to write. 2. Write the pair. 3. `validate_nodes` – fix every error it reports. 4. `preview` – render it. 5. `check_page` – fix what the rendered page actually does wrong. See [Validation](validation.md) for what the reports look like. --- # MCP tools Path: /docs/agents/tools # MCP tools `npx nodality mcp` runs a Model Context Protocol server over stdio with six tools. Every report has the same shape, so an agent repairs from one kind of output. | Tool | Checks or returns | |---|---| | `list_ops` | The vocabulary of `N` | | `get_schema` | The vocabulary of `E`, one type at a time | | `validate_nodes` | Whether a pair is valid, before it renders | | `preview` | The pair rendered to HTML | | `check_page` | What the rendered page actually does | | `parse_html` | A rendered page read back into `E` | ## list_ops Every raster op with its pipeline stage and its parameters (name, default, unit, whether changing it rebuilds the shader), plus the parameters every node inherits (`target`, `by`, `side`, `ease` …), the driver, easing and unit names, the transition `presets` a morph may name, and the `elementTypes`. It is assembled from the live registry, so it cannot drift from what the pipeline accepts. ## get_schema One element type's parameters, on demand. Call it for the type you are about to write rather than carrying every type in context: that is the difference between paying for a schema once and paying for it in every request. Each entry is recovered from the source the components actually read, so it cannot describe a parameter the library does not have. Omit `type` for the whole schema. Composites carry content in a slot – `items` for cards, nav, sideNav, table and ulist; `children` for row, form, stack and wrap. ## validate_nodes Checks a node array – and, if you pass them, the elements and targets – and returns a report. It never throws: malformed input *is* a report. Near misses are checked against the op that was meant, so one call returns every error. See [Validation](validation.md). ## preview Renders an `(E, N)` pair to a self-contained HTML file through the build-time prerenderer (jsdom; no browser is launched) and returns `{ path, bytes, note }`. Invalid nodes return the validation report instead. Raster effects and transitions need a GPU and run only when the file is opened in a browser. Needs `jsdom` in your project. ## check_page What the page actually does once it has real layout, at a phone and a desktop width: horizontal overflow, elements past the viewport, clipped content, images with no description, tap targets under 24 px, controls with no accessible name, skipped heading levels and text below the WCAG contrast minimum. Pass `html`, or `path` to a file `preview` wrote; for a prerendered site page, pass `base` so its fonts and assets load. It reports facts, never taste. Needs Playwright in your project. ## parse_html Reads a rendered page back into an `E` array, so a page can be edited rather than regenerated. A page rendered with `{ annotate: true }` comes back exactly. Without it, only what the tag settles is recovered (headings, paragraphs, links, images, lists); composites that render as a bare `
` are reported as unrecovered rather than guessed. --- # Validation Path: /docs/agents/validation # Validation A misspelled op does not throw – it renders nothing. Content in the wrong slot of a composite renders placeholders. The validator turns those silent failures into a report an agent can act on. ```js import { validateNodes, describeOps } from "nodality/validate"; const report = validateNodes([{ op: "dithr", target: ["hero"] }], elements); ``` ```json { "ok": false, "errors": [ { "code": "UNKNOWN_OP", "path": "nodes[0].op", "got": "dithr", "suggestions": ["dither"], "valid": ["hexalize", "offset", "flow", "…"] } ], "warnings": [] } ``` Each error carries a `code`, the `path` that failed, what it `got`, did-you-mean `suggestions` and the `valid` vocabulary for that position. Pass `elements` too and every `target` is checked against real ids, and the elements themselves are validated. Common codes: | Code | Means | |---|---| | `UNKNOWN_OP` | No op by that name | | `UNKNOWN_PARAM` | The op has no such parameter | | `UNKNOWN_TARGET` | A `target` id that no element has | | `UNKNOWN_ELEMENT_TYPE` | An element `type` that names nothing | | `WRONG_CONTENT_SLOT` | Content in `items` where the type reads `children`, or the reverse | | `MISSING_FIELD` | A required field, such as an element's `type`, is absent | ## Checking the rendered page Valid input can still produce a wrong page. `checkPage` drives a real browser at a phone and a desktop width and reports in the same shape: ```js import { checkPage } from "nodality/check"; // needs Playwright in your project const report = await checkPage(html); ``` Both are also [MCP tools](tools.md). --- # Button Path: /docs/api/button # Button `button.js` ## Options | option | description | |---|---| | `align` | _undocumented_ | | `auto` | _undocumented_ | | `background` | _undocumented_ | | `bold` | _undocumented_ | | `border` | _undocumented_ | | `centered` | _undocumented_ | | `class` | _undocumented_ | | `color` | _undocumented_ | | `exact` | _undocumented_ | | `fluidc` | _undocumented_ | | `frame` | _undocumented_ | | `height` | _undocumented_ | | `img` | _undocumented_ | | `margin` | _undocumented_ | | `onTap` | _undocumented_ | | `padding` | _undocumented_ | | `radius` | _undocumented_ | | `svg` | _undocumented_ | | `type` | _undocumented_ | | `url` | _undocumented_ | | `weight` | _undocumented_ | | `width` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Center Path: /docs/api/center # Center `center.js` ## Options | option | description | |---|---| | `id` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # FlexGrid Path: /docs/api/flexgrid # FlexGrid `flex-grid.js` ## Options | option | description | |---|---| | `animation` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `filtera` | _undocumented_ | | `gradient` | _undocumented_ | | `id` | _undocumented_ | | `layout` | _undocumented_ | | `marginOp` | _undocumented_ | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # FlexRow Path: /docs/api/flexrow # FlexRow `flex-row.js` ## Options | option | description | |---|---| | `animation` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `filtera` | _undocumented_ | | `gradient` | _undocumented_ | | `height` | _undocumented_ | | `id` | _undocumented_ | | `layout` | _undocumented_ | | `marginOp` | _undocumented_ | | `mobile` | _undocumented_ | | `onTap` | _undocumented_ | | `position` | _undocumented_ | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | | `top` | _undocumented_ | | `width` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Image Path: /docs/api/image # Image `image.js` ## Options | option | description | |---|---| | `animation` | _undocumented_ | | `at` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `color` | _undocumented_ | | `filtera` | _undocumented_ | | `gradient` | _undocumented_ | | `height` | _undocumented_ | | `layout` | _undocumented_ | | `marginOp` | _undocumented_ | | `onTap` | _undocumented_ | | `radius` | _undocumented_ | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | | `type` | _undocumented_ | | `width` | _undocumented_ | | `x` | _undocumented_ | | `y` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Link Path: /docs/api/link # Link `link.js` ## Options | option | description | |---|---| | `align` | _undocumented_ | | `animation` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `blast` | _undocumented_ | | `blastData` | _undocumented_ | | `block` | Render as a block-level element rather than inline. | | `bold` | _undocumented_ | | `border` | _undocumented_ | | `clampc` | _undocumented_ | | `class` | _undocumented_ | | `color` | _undocumented_ | | `data` | _undocumented_ | | `fixMobileTap` | Suppress the mobile tap highlight and its 300ms delay. | | `flex` | _undocumented_ | | `fluidc` | _undocumented_ | | `font` | _undocumented_ | | `gradient` | _undocumented_ | | `icon` | _undocumented_ | | `id` | _undocumented_ | | `isNavA` | _undocumented_ | | `marginOp` | _undocumented_ | | `maxWidth` | _undocumented_ | | `new` | Open in a new tab — sets target="_blank" and rel="noopener noreferrer". | | `nowrap` | Keep the label on one line (white-space: nowrap). | | `onTap` | _undocumented_ | | `padding` | _undocumented_ | | `preffersId` | _undocumented_ | | `radius` | _undocumented_ | | `rounded` | Apply the default corner radius (0.5rem). Use `radius` for a specific value. | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | | `tags` | _undocumented_ | | `text` | _undocumented_ | | `type` | _undocumented_ | | `url` | _undocumented_ | | `width` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Picker Path: /docs/api/picker # Picker `picker.js` ## Options | option | description | |---|---| | `background` | _undocumented_ | | `items` | The choices, each a `[value, text]` pair — or a plain string used as both. | | `label` | Accessible name for the control, written to `aria-label`; a select with no adjacent text has none otherwise. | | `name` | Field name submitted with the selected value. | | `radius` | _undocumented_ | | `title` | Accepted as a synonym for `label`, for parity with `labelInput`. | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Text Path: /docs/api/text # Text `text.js` ## Options | option | description | |---|---| | `absolute` | _undocumented_ | | `align` | _undocumented_ | | `alt` | Description for an icon that carries meaning; omit for decoration. | | `animation` | _undocumented_ | | `area` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `block` | _undocumented_ | | `border` | _undocumented_ | | `breakWord` | Allow long words to wrap mid-word (word-wrap: break-word). | | `clampc` | _undocumented_ | | `class` | _undocumented_ | | `color` | _undocumented_ | | `em` | _undocumented_ | | `exact` | _undocumented_ | | `filtera` | _undocumented_ | | `flex` | _undocumented_ | | `fluid` | _undocumented_ | | `fluidc` | _undocumented_ | | `font` | _undocumented_ | | `gpos` | Grid placement — \{col, row\}, written to grid-column and grid-row. | | `gradient` | _undocumented_ | | `height` | _undocumented_ | | `icon` | _undocumented_ | | `id` | _undocumented_ | | `index` | _undocumented_ | | `initLetter` | _undocumented_ | | `italic` | _undocumented_ | | `layout` | _undocumented_ | | `left` | _undocumented_ | | `marginOp` | _undocumented_ | | `maxWidth` | _undocumented_ | | `onScroll` | _undocumented_ | | `onTap` | _undocumented_ | | `preffersId` | Prefer this id when the Designer emits code for the element. | | `removeDecoration` | _undocumented_ | | `responsive` | _undocumented_ | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | | `tag` | Heading level to render, independent of `size`/`fluid`. | | `top` | _undocumented_ | | `type` | _undocumented_ | | `url` | _undocumented_ | | `vtn` | _undocumented_ | | `width` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # TextField Path: /docs/api/textfield # TextField `text-field.js` ## Options | option | description | |---|---| | `background` | _undocumented_ | | `bold` | Shorthand for font-weight bold. Overrides `weight` if both are given. | | `color` | _undocumented_ | | `exact` | Font size as an exact CSS length, e.g. "0.875rem". | | `font` | _undocumented_ | | `id` | _undocumented_ | | `label` | _undocumented_ | | `maxWidth` | _undocumented_ | | `name` | _undocumented_ | | `placeholder` | Placeholder text shown while the field is empty. | | `radius` | Corner radius in pixels. A bare number, not a CSS length. | | `required` | _undocumented_ | | `title` | _undocumented_ | | `type` | Input type attribute. Defaults to "text"; "search" adds the native clear button. | | `weight` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # UList Path: /docs/api/ulist # UList `ulist.js` ## Options | option | description | |---|---| | `animation` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `filtera` | _undocumented_ | | `gradient` | _undocumented_ | | `layout` | _undocumented_ | | `marginOp` | _undocumented_ | | `shadow` | _undocumented_ | | `span` | _undocumented_ | | `stroke` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Wrapper Path: /docs/api/wrapper # Wrapper `container.js` ## Options | option | description | |---|---| | `align` | _undocumented_ | | `animation` | _undocumented_ | | `area` | _undocumented_ | | `at` | _undocumented_ | | `backTo` | _undocumented_ | | `background` | _undocumented_ | | `backgroundOp` | _undocumented_ | | `blast` | _undocumented_ | | `border` | _undocumented_ | | `borderObja` | _undocumented_ | | `centerColumn` | _undocumented_ | | `class` | _undocumented_ | | `color` | _undocumented_ | | `column` | _undocumented_ | | `customAlign` | align-items, verbatim — for values `align` does not cover. | | `customJustify` | justify-items, verbatim. | | `disp` | CSS display, verbatim — "block", "grid", "inline-flex". | | `filter` | _undocumented_ | | `filtera` | _undocumented_ | | `flexCenter` | _undocumented_ | | `flexDir` | Flex direction. Also sets display:flex, so it is enough on its own. | | `font` | _undocumented_ | | `ga` | CSS grid-area, verbatim. | | `gpos` | Grid placement — \{col, row\}, written to grid-column and grid-row. | | `gradient` | _undocumented_ | | `height` | _undocumented_ | | `id` | _undocumented_ | | `isHidden` | Hide the element. | | `layout` | _undocumented_ | | `makeResponsiveBehaviour` | _undocumented_ | | `marginOp` | _undocumented_ | | `maxHeight` | _undocumented_ | | `maxWidth` | _undocumented_ | | `name` | _undocumented_ | | `onTap` | _undocumented_ | | `opacity` | _undocumented_ | | `overflow` | _undocumented_ | | `position` | _undocumented_ | | `radius` | _undocumented_ | | `ranges` | _undocumented_ | | `responsive` | _undocumented_ | | `scale` | _undocumented_ | | `sequence` | _undocumented_ | | `shadow` | _undocumented_ | | `simpleBorder` | Border as one CSS shorthand string, e.g. "1px solid #eee". | | `simpleCenter` | _undocumented_ | | `span` | _undocumented_ | | `sticky` | Stick to the top of the scroll container (position: sticky; top: 0). | | `stretch` | _undocumented_ | | `stroke` | _undocumented_ | | `top` | _undocumented_ | | `transition` | _undocumented_ | | `weight` | _undocumented_ | | `width` | _undocumented_ | | `zIndex` | _undocumented_ | ## Inherited from Animator Accepted by every component. Defined once in `animator.js`. | option | description | |---|---| | `center` | Centres this element's CHILDREN. `true` for both axes, `"x"` horizontal, `"y"` vertical. Axis-aware: in a flex column `"y"` is justify-content, in a row it is align-items; a grid uses justify-items/align-items. To centre the element itself inside its parent, use `mar: "center"`. | Generated from nodality 1.4.0. --- # Basics Path: /docs/basics/README # Basics This library employs an unique approach to building static websites using declarative UI components and nodes that will style them. [Example →](start.html) --- # Animation Path: /docs/basics/animation # Animation Create striking animations using JS responsive API. ```js new Text("Hello") .set({ fluidc: "S1", animation: { op: { name: "animation", color: "green", width: "1px", keyframesOpen: [{ transform: "translate(100%, 0%)", opacity: 0 }, { transform: "translate(0%)", opacity: 1 }], keyframesClose: [{ transform: "translate(0%)", opacity: 1 }, { transform: "translate(100%, 0%)", opacity: 0 }], openOptions: { duration: 300, fill: "forwards", delay: 1000 }, closeOptions: { duration: 1, fill: "forwards", delay: 1000 } } }, }).render("#res"); ``` --- # CDN Path: /docs/basics/cdn # CDN To use Nodality with CDN, use this code: ```html
``` ## Demo --- # Custom elements Path: /docs/basics/customElements # Custom elements To create a custom element using Nodality library, you extend the ```Base``` class. We can supply our properties in constructor, and assign the to ```props``` property. In the ```render``` method we then return our [```Text```](../elements/text.md) element. ```js class MyApp extends Base { constructor(props) { super(); this.props = props; } render(){ return new Text(this.props.year) .set({ color: "#1abc9c" }); } } ``` We can pass any object into the class instance. We then call the ```mount``` method to render the ```Text``` elment in our DOM. ```js new MyApp({ year: "2025" }) .mount("#mount"); ``` The HTML to be mounted is following: ```html
``` --- # Designer Path: /docs/basics/designer # Designer Designer instance accepts nodes and elements arrays. It adds elements to the page and displays the code output. ```html
``` ```js new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount" }); ``` If you want to hide the code, you can set ```code``` property to ```false``` inside ```set``` method: ```js ... .set({ code: false, mount: "#mount" }); ``` --- # Nodes Path: /docs/basics/nodes # Nodes Nodes allow you to simply you to modify the elements. Think of them as nodes in procedural software like Houdini. Every node generates code that is used on elements. ### Step 1: Defining a node Blast node allows you to create stroked text. ```js let nodes = [ { op: { name: "blast" } } ] ``` If you want, you can specify more options for your node like ```color``` and ```width``` for the ```blast``` node. ```js let nodes = [ { op: { name: "blast", color: "green", width: "3px" }, } ] ``` ### Step 2: Defining ranges Breakpoints allow you to control the range where the node will take effect. You can use the ```range``` key for this purpose. Range takes a form of array with two values Using ```range``` is not mandatory. ```js let nodes = [ { op: {/*...*/}, range: ["600px", "700px"], } ] ``` ### Step 3: Affecting specific elements You can also decide to affect specific elements. In this example, only element with ```id``` ```#myID``` will be affected. If you don't supply any value to the ```target``` property, all nodes for which this operations applies will be affected. The ```id``` property is not mandatory. Let's define element with certain ID: ```js let elements = [ { type: "text", id: "#first" } ]; ``` We then use the ```target``` property to affect only this element. ```js let nodes = [ { op: {/*...*/}, range: [ /*...*/ ], target: ["#first"] } ]; ``` ### Seeing it all together The complete ```nodes``` with blast node might look like this. Keep in mind that range and target properties are not mandatory. ```js let elements = [ { type: "text", id: "#first" } ]; let nodes = [ { range: ["600px", "700px"], op: { name: "blast", color: "green", width: "3px" }, target: ["#first"] } ] ``` Then, you can render the result as you are used to: ```html
``` ```js new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount" }); ``` --- # Getting started Path: /docs/basics/start # Getting started This library works with *elements* represented as an array of HTML entities, and *nodes* that control the behavior of elements. :::tip Built for agents Nodality is written by people **and by coding agents**. One command – `npx nodality skill` – sets up Claude Code or Cursor with the Nodality skill and an MCP server that checks every page before it renders. [Agents – getting started →](../agents/start.md) ::: **Elements** is an array of objects. This array produces the code of elements you can use in your website. **Nodes** is another array containing nodes that change the look and behavior of the generated elements. Let's walk through the app creation process step-by-step. There is also interactive playground at the [bottom](#demo) of this page. ### Installation The fastest way to get started is to use the NodeJS. You can also use the library in a browser via [CDN](cdn.md). ```js npm create nodality@latest my-app ``` After the tool creates the application, navigate to my-app directory using ```cd my-app```. In the ```src``` directory you will find the ```app.js``` file. Let's go through it step-by-step. ### Step 1 Define array of **elements** you want to display in your user interface. In this example, we will create an ```h1``` element with "Hello" text. ```js let elements = [ { type: "h1", text: "Hello" } ]; ``` ### Step 2 Define array of **nodes** that will adjust behaviour of the element. This particular node will add the stroked text effect. ```js let nodes = [ { op: "blast" } ]; ``` ### Step 3 Add ```nodes``` array into ```.nodes``` modifier, and use ```.set``` method to render output of your code. Use ```code: true``` option to also display the code of the elements. ```js new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount", code: true }); ``` Also define ```div``` with id ```#mount``` that will serve as a main point to render the UI. To compile (and recompile) the app after changes, run: ``` npm run build && npm start ``` ## Everything together Here is the entire code using CDN: ```html
``` After running this code, you will see ```h1``` element on the screen. When the user resizes window anf hits the breakpoint 400 - 600px the text stroke effect will appear thanks to stroke modifier. User will also see the resulting code. ![output](image.png) ![alt text](mygif.gif) ```js , new Text("Hello") .set({ index: "0", fluidc: "S1", font: "Arial", stroke: {op:{name:"blast",color:"green",width:"1px"}}, }) .render("#mount"); ``` You can then render this generated element directly in new empty HTML file. Use the ```render``` method, where ```#mount``` is a ```div``` element that will be used to render the ```Text``` class. ```html
``` See the demo below: ## Demo --- # Card Path: /docs/elements/cards # Card Create ```Card``` elements containing ```Image```, ```Text``` and ```Link``` elements. If we supply elements into the ```children``` property, the respective elements are created. * ```image``` - ```new Image...``` * ```text``` - ```new Text...``` * ```link``` - ```new Link...``` ```js let elements = [ { type: "cards", children: ["image", "text", "link"] } ]; ``` ![cards](image-1.png) This operation results in this code. ```js new FlexGrid() .set({ colat: "700px", wrap: true, align: "center", gap: "1rem" }) .items( [ { img: "https://upload.wikimedia.org/wikipedia/commons/3/3a/Starship_S20.jpg", title: "Starship", link: "#ship" }, { img: "https://upload.wikimedia.org/wikipedia/commons/1/16/Apollo_11_Launch_-_GPN-2000-000630.jpg", title: "Saturn V", link: "#saturn" }, { img: "https://upload.wikimedia.org/wikipedia/commons/d/d6/STS120LaunchHiRes-edit1.jpg", title: "Shuttle", link: "#shuttle" } ].map(item => new Card() .set({ width: "300px", height: "700px", radius: "0.7rem", mar: { sides: ["all"], value: "0.8rem" } }) .items([ new Image(item.img).set({isFull: true, url: item.img}), new Text(item.title).set({ size: "S5", color: "orange" }), new Link("Link").set({ text: item.title, url: item.link, background: "#3498db", pad: [{ lr: "0.5rem", tb: "1rem" }], radius: "0.4rem", color: "white" }) ]) ) ) .render("#mount"); ``` # ZoomCard To use image as background for the entire card use the ```ZoomCard``` element with a background. To create entire grid with these cards, use the following operator: ```js let els = [ { type: "cards", children: ["text", "image", "link"], backgroundCard: true } ]; ``` ![cards](image.png) This generates ```FlexGrid``` with ```ZoomCard``` structure. The ```inpad``` property sets the inner padding. //182501 images ```js new FlexGrid() .set({ colat: "700px", wrap: true, align: "center", gap: "1rem" }) .items( [ { img: "https://upload.wikimedia.org/wikipedia/commons/3/3a/Starship_S20.jpg", title: "Starship", link: "#ship" }, { img: "https://upload.wikimedia.org/wikipedia/commons/1/16/Apollo_11_Launch_-_GPN-2000-000630.jpg", title: "Saturn V", link: "#saturn" }, { img: "https://upload.wikimedia.org/wikipedia/commons/d/d6/STS120LaunchHiRes-edit1.jpg", title: "Shuttle", link: "#shuttle" } ].map(item => new ZoomCard() .set({ url: item.img, font: "Arial", mar: { sides: ["all"], value: "0.8rem" }, inpad: "1rem", useBrightness: true }) .items([ new Text(item.title).set({ fluidc: "S6", color: "orange" }), new Link("Link").set({ text: item.title, url: item.link, background: "#3498db", pad: [{ lr: "0.5rem", tb: "1rem" }], radius: "0.4rem", color: "white" }) ]) ) ) .render("#mount"); ``` --- # Circle Path: /docs/elements/circle # Circle You can use the ```circle``` element to display a simple circle. ## Syntax ```js let elements = [ { type: "circle" } ]; ``` ## Rendered Element The instance of ```Circle``` class will be created. ```js new Circle().set({ diameter: 108, background: "#1abc9c", }).render("#mount"); ``` --- # Code Path: /docs/elements/code # Code You can use the ```code``` element to display a block of code. ## Syntax ```js let elements = [ { type: "code" } ]; ``` ## Rendered Element The instance of ```Code``` class will be created. ```js new Code() .set({ class: "language-js", code: `let text = new Text("Hello").set({})` }), ``` In this example, we set the class to ```language-js``` to the code element. When use in conjuction with ```hljs``` library, the syntax will be correctly highlighted. We need to use ```hljs.highlightAll()``` method, to use its syntax highlighting. ```html ``` ```js hljs.higlightAll() ``` --- # Dropdown Path: /docs/elements/dropdown # Dropdown Dropdown is used to create expand area upon click or hover. The first value is used as a title for the dropdown - in this case text "Options". You can also use this element in [NavBar](navbar.md). ## Syntax ```js let elements = [ { type: "dropdown", text: "Options", items: ["Earth", "Sea", "Fire"] } ]; ``` ## Rendered Element ## `behaviour` Argument The `behaviour` property controls **how the dropdown menu is triggered** by the user. **Accepted values:** | Value | Description | |--------------|-------------| | `click` | Default. The dropdown opens when the user clicks on the dropdown element. | | `mouseover` | The dropdown opens when the user hovers the mouse over the dropdown element. | ```js new Dropdown() .set({ behaviour: "mouseover", // click otherwise padding: "10px", border: "1px solid black", }) .add([ new Text("Options"), new Text("Earth"), new Text("Sea"), new Text("Fire"), ]); ``` --- # Free Path: /docs/elements/free # Free Create free alement to use in conjuction with [Layout](/library/nodes/layout) node. See [Layout](/library/nodes/layout) node for more information. ```js let elements = [ { type: "free" } ]; ``` --- # AreaGrid Path: /docs/elements/grid # AreaGrid The `grid` creates an instance of the `AreaGrid` element, designed to organize child elements in a grid layout. ```js let elements = [ { type: "grid" } ]; ``` In ```set``` method of this class, we can use the ```gap``` property to set gap within the grid. The react method allows the grid to respond to breakpoints and dynamically adjust its layout. ### Arguments: ```js | Argument | Description | |-----------|-------------| | `at` | The breakpoint (in pixels) at which this template becomes active. | | `template`| An array of strings defining the grid areas for this breakpoint. Each character represents a grid area. | | `color` | (Optional) Sets the color of the grid items at this breakpoint. | ``` In ```Wrapper``` class, in this example we can use the ```mc``` property sets the height to ```max-content```. ```mc: true``` means sets this css element ```height``` to ```max-content```. ```js new AreaGrid() .set({ gap: "1rem" }) .react([{ at: "0px", template: [ "eeeeee", "eeeeee", "abbbbb", "abbbbb", "abbbbb", "cccccc", "cccccc", "dddddd" ] }, { at: "900px", color: "orange", template: [ "aaaccc", "aaaccc", "bbbbbb", "dddddd", "eeeeee" ] }, { at: "1200px", color: "purple", template: [ "cccaaa", "cccaaa", "bbbbbb", "dddddd", "eeeeee" ] }, ]) .add([ new Wrapper().set({ mc: true, pad: [{ a: 20 }], background: "#ecf0f1", radius: "1.7rem" }).add([ new Text("NEW PRODUCT").set({ exact: "1rem", font: "Oswald", width: "100%" }), new Text("Feel").set({ fluidc: "S2", font: "Oswald", width: "100%", color: "black" }), new Text("Speed").set({ exact: "7rem", font: "Oswald", width: "100%", color: "#3498db" }), ]), new Wrapper().set({ mc: true, pad: [{ a: 20 }], background: "#ecf0f1", radius: "1.7rem" }).add([ new Text("TOP QUALITY").set({ exact: "1rem", font: "Oswald", width: "100%" }), new Text("You can count on it").set({ fluidc: "S2", font: "Oswald", width: "100%", color: "black" }), new Text("Experience the thrill of open water swimming with our exceptional performance. So whether you’re participating in a triathlon or simply enjoying a leisurely swim, you can trust our wetsuits to enhance your performance and elevate your experience.So why wait? Dive into your next open water adventure with our top-of-the-line wetsuits. Make waves, set new records, and explore the uncharted depths of the ocean with confidence and style. Experience the difference today with our exceptional wetsuits!") .set({ font: "Arial", exact: "1.3rem" }) ]), new Wrapper().set({ mc: true, radius: "1.7rem", overflow: "hidden" }).add([ new Image("https://www.blueseventy.com/cdn/shop/files/23-home-size2.jpg?v=1684272215&width=3840", "exact").set({ width: "100%", height: "420px", minHeight: "100px", objectFit: "cover" }) ]), new Wrapper().set({ mc: true, pad: [{ a: 20 }], background: "#ecf0f1", radius: "1.7rem", }).add([ new Text("You won't miss with Blue70").set({ fluidc: "S3", font: "Oswald", color: "black" }), new Text("Yea") .set({ font: "Arial", exact: "1.3rem" }) ]), new Wrapper().set({ mc: true, pad: [{ a: 20 }], background: "#ecf0f1", radius: "1.7rem", }).add([new Text("Hello").set({ font: "Arial", exact: "1.3rem" }), ]), ]).render("#mount"); ``` --- # HScroller Path: /docs/elements/hscroller # HScroller You can use the ```HScroller``` to make a horizontally scrolling gallery. ## Syntax ```js let elements = [ { type: "hscroller" } ]; ``` ## Rendered Element The instance of ```HScroller``` class will be created. ```js new HScroller() .seto({ height: "300px", speed: 1.0 // the bigger the slower }) .add([ new Text("First").set({ size: "S1", pad: [{ "a": 40 }] }), new Text("Second").set({ size: "S1", pad: [{ "a": 40 }] }), new Text("Third").set({ size: "S1", pad: [{ "a": 40 }] }) ]) .render("#mount"); ``` --- # Image Path: /docs/elements/image # Image Image allows you to create simple image ```js let elements = [ { type: "img", width: "300px", url: "https://picsum.photos/200" } ]; ``` The element code will produce the following output: ```js new Image().set({ type: "img", width: "300px", url: "https://picsum.photos/200" }).render("#mount"); ``` # Image as a div background If you want to create div background image set to supplied url, you can use the ```background``` option. ```js { type: "img", isFull: true, url: "sun.png" } ``` This results in the following element. The default height is ```200px```, but you can easily overwrite it by setting the ```width``` property. ```js new Image().set({ isFull: true, url: "sun.png" }).render("#mount"); ``` --- # Keyset Path: /docs/elements/keyset # Keyset Keyset allows you to set custom CSS properties on elements. ## Properties * ```key``` - The name of the property * ```value``` - The value of the property In this example, we use ```border``` as our ```key```. This property is used to change CSS border property. As for the ```value``` property we use ```3px solid green``` to set the border. You are free to use any CSS property you want. ```js new Text("Hello") .set({ fluidc: "S1", font: "Arial", keySet: { key: "border", value: "3px solid green" } }); ``` --- # Link Path: /docs/elements/link # Link To create a simple link use the following element. ```js let elements = [ { type: "a", text: "Hello", link: "https://www.nasa.gov" } ]; ``` This generates the following element: ```js new Link() .set({ pad: [ { a: 10 } ], font: "Arial", link: "https://www.nasa.gov", text: "Hello", url: "https://www.nasa.gov" }) .render("#mount"); ``` --- # Modal Path: /docs/elements/modal # Modal To create a simple modal. use ```modal``` structure This generates instance of ```Modal``` class and open/close button. ```js let elements = [ { type: "modal" } ]; ``` This code generates the ```Modal``` instance and open/close button. ```js let elements = [ new Stack() .setup({}) .add([ new Image("https://pbs.twimg.com/media/DwYvbCBVAAEKY_R.jpg").set({}), new FlexRow().set({aligns: "start"}).items([ new Button("×") .setup({}) .set({ fluidc: "S3", onTap: () => modal.close(), frame: { width: 80, height: 80 }, color: "white", background: "none" }), new Spacer(true), ]) ]), new Text("Samuel Suresh") .set({ color: "#00ae56", font: "SF Pro Display", fluidc: "S2", pad: [{tl: 20}] }), new Text("Studying Science and Business, Western Sydney University, class of 2022") .set({ font: "Arial", fluidc: "S6", pad: [{l: 20}] }), new Text(firstLong) .set({ initLetter: 1 }) .pad([{t: 30}]) // THIS IS VALID !!! .pad([{lrb: 30}]) ]; let wrapper = new Wrapper() .set({}) .add(elements); let modal = new Modal(); modal .set({ width: "600px", background: "#469d73cc", close: true }) .add([wrapper]) // .close() .render("#res"); new Button("Wow") .setup({}) .set({ fluidc: "S3", onTap: () => modal.show() }) .render("#res"); ``` --- # Navigation Path: /docs/elements/navigation # Navigation To create a site navigation use the `nav` element. It shows the full bar on wide screens and collapses the links behind a menu button below its breakpoint. ```js const elements = [ { type: "nav", id: "nav", brand: "Acme", // or an element spec: { type: "p", text, keySet, … } items: [ { title: "Work", link: "#work" }, { title: "About", link: "#about" }, { title: "Contact", link: "#contact" }, ], breakpoint: 768, // the full bar from here up (default "1200px") font: "Inter", color: "#111", hover: { color: "#b83a0b" }, background: "rgba(255, 255, 255, 0.8)", radius: "14px", mar: [{ a: 0 }], pad: [{ tb: 12, lr: 24 }], keySet: [{ key: "backdropFilter", value: "blur(20px)" }], menuLabel: "Menu", }, ]; ``` ### Options | option | what it does | |---|---| | `items` | The links, each `{ title, link }` or a plain string. Declaring them is what builds the nav from these options. | | `brand` | The name at the start of the bar — a string, or an element spec for full control of its type. | | `breakpoint` | Width from which the full bar shows; below it the links collapse behind the menu button. A number is px. Default `"1200px"`. | | `font`, `color` | The links' font and colour (and a string brand's). `color` also colours the menu button unless `hamburgerColour` is given. | | `hover` | Link hover styles, as on `a`. | | `background`, `radius`, `mar`, `pad` | The bar itself. Default margin `[{a: 21}]`; pass `[{a: 0}]` when a wrapper positions the bar. | | `keySet` | Extra CSS on the bar, desktop and mobile — e.g. `backdropFilter`, `boxShadow`, `fontSize`. | | `hamburgerColour` | Colour of the menu button. | | `menuLabel` | Accessible name of the menu button. Default `"Menu"`. | `get_schema nav` lists each of these with its shape, and `validate_nodes` checks them. ### Behaviour - The menu button is a real ` ); } ``` A rebuild replaces the view's elements, so a field inside it loses focus when its `deps` change. Leave values that change on every keystroke out of `deps`. ### A specification: `elements` and `nodes` Pass the `(E, N)` pair as data: ```jsx import Nodality from "nodality/react"; const elements = [ { type: "wrap", id: "hero", children: [ { type: "h1", id: "title", text: "Declared as data" }, ]}, ]; const nodes = [ { op: "blast", target: ["title"], color: "#1D6FE0", width: "2px" }, ]; export default function Hero() { return ; } ``` ### The difference that matters Only a specification survives the build. Because it is data, the component writes it into the page next to its host (` // 13:21:24 26/04/2024 ``` ### HelloWorld.vue ```js ``` --- # Testing Path: /docs/guides/testing # Testing Nodality uses Playwright for testing. To test, run the following commands: ```sh git clone https://github.com/nodalityjs/nodality cd nodality npm install npm test ``` --- # Tooling Path: /docs/guides/tooling # Tooling ### Create nodality Create Nodality package allows you to quickly bootstrap a new Nodality application. For deploying instructions see [Deploy](deploy.md). To create simple app use: ```sh npm create nodality@latest my-app ``` Navigate to your app: ```sh cd my-app ``` Simple Nodality app will be created. It consists of several files here are the most important: * ```index.html``` - basic HTML with ```
``` where the app will be created. * ```src/app.js``` - Your Nodality app code #### index.html ```html my-app
``` #### /src/app.js ```js import * as Nodality from "nodality"; const elements = [ { type: "h1", text: "Hello" } ]; const nodes = [ { op: "blast" } ]; new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount", code: true, }); ``` ### Nodality CLI Nodality CLI allows you to quickly scaffold a Nodality app and add components using command line interface. 1) First make new NodeJS project: ```sh npm init -y ``` 2) Install CLI: ```sh npm i nodality cli ``` 3) Make new project: ```sh nodality-cli create projectName cd projectName npm run build npm start ``` 4) Add a component: ```sh nodality-cli component Nice ``` The following component will be generated in ```Nice.js``` file: ```js import * as Nodality from "nodality"; class Nice extends Base { constructor(props) { super(); this.props = props; } render(){ return new Text(this.props.year) .set({ color: "#1abc9c" }); } } export { Nice }; ``` You can now add the component into your app. ```html App
``` To recompile the app after changes, run: ```sh npm run build && npm start ``` ### Create Nodality React Scaffolds a React + Vite app that uses the `nodality/react` component and prerenders specifications to HTML at build time. For more see: [Integration](integration.md) ```sh npx create-nodality-react my-app ``` ### Create Nodality Vue Nodality support Vue integration. For more see: [Integration](integration.md) ```sh npx create-nodality-vue my-app ``` --- # Transitions Path: /docs/guides/transitions # Transitions This library supports the **CSS View Transition API** to enable smooth animations when navigating between pages.\ For example, you can build a carousel where each card smoothly transitions into a detailed view. :::warning The View Transition API is available in every modern browser with the exception of Firefox. See [here](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API#browser_compatibility) for browser compatibility. ::: :::warning The View Transition API does not work well when developing using web server. For better experience open your app in the browser with CORS disabled. Example for Chrome on macOS: ```sh open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security ``` ::: ## Step 1 Create a new app using CLI: ```sh npm create nodality@latest my-app ``` ## Step 2 - Define the card component ### `index.html` 1. **Set up the environment** - Add the `meta` tag with `view-transition` set to `same-origin`. - Declare `@view-transition { navigation: auto; }` in your CSS. This enables automatic page navigation transitions. 2. **Create a card** - The `vtn` attribute is shorthand for `view-transition-name`. - A matching `vtn` value must exist both in the **parent view (index.html)** and in the **child view (detail page)** for the transition to animate correctly. 3. **Use the `HScroller`** - The `HScroller` class is used to build a horizontally scrolling card carousel. - Configure scrolling speed using `.seto({ speed })` (higher values = slower scrolling). - Add multiple card instances with `.add([...])`. Each card should have a unique identifier (`id` or `textChild`) to ensure smooth transitions. ```html
``` # Step 3 - Create the detail view ### `child-A.html` The detail page is where the user is taken after clicking on a card. ```js ``` ### Step 4 - Use detail.js In `detail.js`, you can: - Render a title, text, and images. - Animate elements (e.g., header text sliding in). - Provide navigation back to the index page (e.g., with a back arrow). - Use the same `vtn` value (`iconimg`) for the image to link the transition between **index.html** and **child-A.html**. The code already:\ - Creates a heading and animates it. - Adds a back button (`Link("Hello", "index.html")`). - Renders the same image with the same `vtn` name. - Appends multiple paragraphs of text to simulate a content-rich detail view. ```js let r = new Center().items( [ new Text("Best ship") .set({ font: "Arial", clampc: "S1", em: 4.2, color: "white", weight: "bold", removeDecoration: true, id: "header", mar: [{t: "7rem"}], }) ] ) let ropo = document.createElement("a"); ropo.setAttribute("href", "index"); let l = new Link("Hello", "index.html").set({ link: "index.html", data: { options: { url: "index.html", img: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Back_Arrow.svg/2048px-Back_Arrow.svg.png", size: "70px", radius: "0.1rem" } }, }); let flex = new FlexRow().items([ l, new Spacer(true), ]); let ela = flex.render(); ela.style.marginTop = "1rem"; document.body.appendChild(ela); let el = r.render(); document.body.appendChild(el); document.querySelector("#header").style.transform = "translateY(200px)"; document.querySelector("#header").animate([ { opacity: "1", opacity: "0" }, { transform: "translateY(200px)", transform: "translateY(0px)", }, ], { duration: 800, fill: 'forwards' }); let a = document.createElement('a'); a.href = 'index.html'; let e = new Image("https://media.cnn.com/api/v1/images/stellar/prod/ap24002766263400.jpg?c=original", 'exact').set({ url: 'https://media.cnn.com/api/v1/images/stellar/prod/ap24002766263400.jpg?c=original', class: "img-small", width: "900px", height: "600px", mar: [{t: "7rem"}], vtn: "iconimg" }); let ra = new Center().items( [ e ]); let tz = ra.render(); a.appendChild(tz); document.body.appendChild(a); let paragraph = new Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam a maximus massa. Nam at egestas ante, vitae posuere turpis. Suspendisse non metus sagittis, tristique sem id, pellentesque urna. Phasellus tempus quam at tellus suscipit, non maximus leo euismod. Nunc quis facilisis turpis. Donec tristique vel felis eu finibus. Maecenas malesuada rutrum est pulvinar dapibus. Praesent eu sem condimentum, posuere turpis sit amet, mollis libero. Praesent nulla nibh, sagittis sed justo ut, facilisis malesuada odio.") .set({ font: "Arial", clampc: "S1", em: 1.2, color: "white", weight: "bold", removeDecoration: true, id: "header", mar: [{lr: "auto"}], width: "50%", pad: [{bt: "2.6rem"}] }); document.body.appendChild(paragraph.render()); let paragraphB = new Text("Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam vulputate nunc a turpis tristique vestibulum. Aliquam pretium aliquam risus, ac laoreet eros ullamcorper et. Maecenas elementum libero vestibulum cursus fringilla. Sed facilisis, nibh et convallis pharetra, eros nisl placerat risus, eget varius magna arcu sit amet dui. Pellentesque consectetur nulla sit amet arcu tristique varius. Nullam at elit sit amet lectus cursus tempus vel et ligula. Pellentesque a tincidunt felis, vitae semper enim. Integer luctus nunc nec elementum dignissim. Nam aliquet euismod quam, non pellentesque arcu tristique porta.") .set({ font: "Arial", clampc: "S1", em: 1.2, color: "white", weight: "bold", removeDecoration: true, id: "header", width: "50%", mar: [{lr: "auto"}], pad: [{b: "2.6rem"}] }); let paragraphC = new Text("Nullam a ornare eros, a vestibulum nulla. Aenean cursus dui pharetra, pellentesque leo eu, euismod tortor. Curabitur tempor mollis lorem, in dignissim nisl convallis at. Etiam dictum, ex eu tempus varius, massa purus malesuada ipsum, elementum aliquet sem quam ut ante. Quisque sed facilisis lacus. Curabitur aliquet egestas nisl sit amet malesuada. Cras vitae finibus ligula, vitae vestibulum magna. Cras molestie ac elit eu aliquam. Phasellus posuere sit amet tellus ut mattis. Quisque blandit at lacus at hendrerit.") .set({ font: "Arial", clampc: "S1", em: 1.2, color: "white", weight: "bold", removeDecoration: true, id: "header", width: "50%", mar: [{lr: "auto"}], }); document.body.appendChild(paragraph.render()); document.body.appendChild(paragraphB.render()); document.body.appendChild(paragraphC.render()); ``` ### Key Notes - **`vtn` consistency**: The `vtn` attribute (shorthand for `view-transition-name`) must match between the originating element and its target for the transition to animate properly.\ - **Navigation**: The `@view-transition { navigation: auto; }` CSS rule makes browser navigation (`a href="..."`) eligible for transitions.\ - **Fallback**: In browsers without View Transition API (e.g., Firefox), navigation will still work, but without smooth transitions.\ - **Customization**: You can add animations (e.g., `.animate([...], { duration })`) for fine-grained control of how elements enter or leave the page. --- # Animation Path: /docs/nodes/animation # Animation Animation allows you to animate elements. To create basic animation, use ```animation``` operator. ### 1 - elements ```js let elements = [ { type: "h1", text: "Miami", id: "#first" } ]; ``` ### 2 - nodes The operator generates ```animation``` property on each affected object. If we supply the ```target``` argument, this operation applies to elements whose ```id``` attributes mathch those in ```target``` array. This animation will apply to element with ```id``` ```#first``` in this case. ```js let nodes = [ { op: "animation", target: ["#first"] } ]; ``` This results in animation property being added to respective element. You can edit this animation as you wish. ### Arguments * ```keyframesOpen``` - set opening keyframes * ```keyframesClose``` - set closing keyframes * ```openOptions``` - set opening properties (duration, etc.) * ```closeOptions``` - set closing properties * ```fireAt``` - how much user needs to scroll before animation fires (optional) Possible value of ```fireAt``` is extact value like ```800px``` or ```inview```. The ```inview``` property fires animation when it comes into view using ```IntersectionObserver``` API. ```js new Text("Hello").set({ // Other properties can go here, e.g., font, size, index... animation: { op: { name: "animation", color: "green", width: "1px", fireAt: "800px", keyframesOpen: [ { transform: "translate(100%, 0%)", opacity: 0 }, { transform: "translate(0%)", opacity: 1 } ], keyframesClose: [ { transform: "translate(0%)", opacity: 1 }, { transform: "translate(100%, 0%)", opacity: 0 } ], openOptions: { duration: 300, fill: "forwards", delay: 1000 }, closeOptions: { duration: 1, fill: "forwards", delay: 1000 } }, target: ["#first"] } }); ``` --- # Responsive – breakpoints from content Path: /docs/nodes/arrange # Responsive – breakpoints from content *Experimental, opt-in. New in 1.4.0.* A row of three headings fits side by side on a laptop and overflows on a phone. The usual fix is a breakpoint at a device width – 768 px, say – that has nothing to do with this row: too early for short text, too late for long text. Nodality can instead **measure where the row stops fitting** and switch exactly there. Two parts: - the **`arrange` node** switches a group's arrangement inside a width range; - **`respond()`** from `nodality/responsive` measures the page in a browser and writes those nodes for you. ## The `arrange` node ```js let nodes = [ { op: { name: "arrange", mode: "column" }, target: ["features"], range: [0, 424] }, { op: { name: "arrange", mode: "row" }, target: ["features"], range: [425, 99999] }, ]; ``` | Group | Modes | Effect | |---|---|---| | `row` | `column` · `row` | stacked, or side by side | | `cards` | `column` · `row` | one card per line, or the grid | | `nav` | `collapse` · `expand` | menu button, or the full bar | Ranges are inclusive CSS pixels of the viewport width. If two ranges on one element overlap, the first declared wins. An `arrange` node replaces the group's built-in switch (700 px for cards, 1200 px for a nav), and nodes you wrote yourself always come before the synthesized ones. You can write these by hand. You do not have to guess the numbers. ## `respond()` – solving the widths `respond()` renders the page at several widths in a browser, measures each group's **content** – its widest unbreakable content on one line, gaps and padding – against the width its parent gives it, and finds the exact pixel where the wide arrangement stops fitting. It returns your elements and nodes with one `arrange` node per interval, covering every width. It needs Playwright in your project (`npm install --save-dev playwright`) and runs at build time, never in the visitor's browser. ```js import { chromium } from "playwright"; import { respond, harnessHTML } from "nodality/responsive"; const spec = { elements: [ { id: "features", type: "row", children: [ { type: "h2", text: "Plain data in" }, { type: "h2", text: "Checked before it renders" }, { type: "h2", text: "Same output every time" }, ] }, ], nodes: [], }; const html = harnessHTML("https://unpkg.com/nodality@1.4.0/dist/index.esm.js"); const browser = await chromium.launch(); const page = await browser.newPage(); // Render `s` fresh at `width`; respond() calls this for every measurement. const renderAt = async (width, s) => { await page.setViewportSize({ width, height: 800 }); await page.setContent(html); await page.waitForFunction(() => window.__nodalityReady === true); await page.evaluate((x) => window.__nodalityRender(x), s); await page.evaluate(() => new Promise((r) => setTimeout(() => requestAnimationFrame(r), 20))); }; const out = await respond(page, spec, renderAt); await browser.close(); console.log(out.synthesized); ``` For this row it prints the two nodes shown above: a column up to 424 px, a row from 425 px. Ship `out.elements` and `out.nodes` like any other pair. ## What it returns | Field | Contains | |---|---| | `elements` | Your elements, with ids added to groups that had none | | `nodes` | Your nodes first, then the synthesized `arrange` nodes | | `synthesized` | Only the new `arrange` nodes | | `report` | Per group: the measured samples, the intervals, widths where no arrangement fits (`infeasible`) | | `measurements` | How many renders it took | | `converged` | Whether nested groups settled (see below) | | `skipped` | Groups left alone, with the reason | ## Behaviour worth knowing - **Only switches where the layout must change.** A group that fits at every width gets one `row` node over the whole range and no breakpoint. - **Deterministic.** No randomness, no clock: the same page always gives the same nodes. - **Your own responsive settings win.** A group that already has `colat`, `column`, `resprop` or a nav `breakpoint` is skipped, and so is a group with fewer than two members. - **Nested groups** are solved top-down, then solved again until nothing changes (at most four passes). `converged: false` means they had not settled by then; the nodes are still valid, the switch widths may be a pixel or two off. - **Some widths cannot fit.** If a single member is wider than its group – a long unbreakable word, a fixed-width image – no arrangement fits there. The report lists those widths as `infeasible` instead of pretending. - **A nav can collapse only below one width.** If its bar fails to fit on separate intervals, it collapses up to the last of them (`widened` in the report). --- # Blast Path: /docs/nodes/blast # Blast Blast allows user to create outline around element contents. You can use elements like ```Text``` and ```Link```. ```js let nodes = [ { op: "blast" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` We then use the element in ```Designer``` instance as we are used to. ```js new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount" }); ``` This generates defaults ```stroke``` operation on each affected element. ```js new Text("Hello").set({ stroke: { op: { name: "blast", color: "green", width: "1px" } } }); ``` ## Custom blast We can also specify our own blast options. ```js let nodes = [ { op: { name: "blast", color: "#1abc9c", width: "12px" } } ]; ``` --- # Copy Path: /docs/nodes/copy # Copy ## Default copy To use the copy operation, we use these nodes: ```js let nodes = [ { op: "copy" } ]; let elements = [ { type: "copy", } ]; ``` This generates defaults ```Wrapper``` with rotated ```Text``` elements. You use ```static: true``` property and ```keep: true``` property to transform element without animation. ```js new Wrapper() .set({ width: "300px", height: "300px", display: "flex", justifyContent: "center", alignItems: "center", position: "absolute", id: "#first", scale: 0.3, }) .add( Array.from({ length: 7 }).map((_, i) => { const bbox = {width: "300px", height: "300px"}; const elHeight = bbox.height; const cx = 150; const cy = 150; const R = 150; const n = 7; // Evenly spaced angles, starting at top (-90°) const angle = (i / n) * Math.PI * 2 - Math.PI/2; const dist = R + parseFloat(elHeight)/2; const x = cx + dist * Math.cos(angle); const y = cy + dist * Math.sin(angle); let rot = angle * 180 / Math.PI + 90; rot = (rot % 360 + 360) % 360; rot = (rot % 360 + 360) % 360; return new Text("Hello").set({ size: "S1", color: "green", font: "Arial", left: `${x}px`, top: `${y}px`, transform: { op: { name: "transform", transform: { static: true, keep: true, values: [ "tx:-50%", "ty:-50%", `rotate(${rot}deg)` ] } } } }) }) ).render("#mount"); ``` ## Animating items You can also add animation using the ```animation``` key. You can then customize the [animation](animation.md) in the generated code. ```js let nodes = [ { op: "copy", animation: true } ]; ``` --- # Filter Path: /docs/nodes/filter # Filter ## Default filter To use the filter, we use this: ```js let nodes = [ { op: "filter" } ]; let elements = [ { type: "img" } ]; ``` This generates defaults grayscale operation on each element. ```js new Image("url") .set({ filtera: { op: { name: "filter", filter: "grayscale(0.7)" } } }); ``` ## Custom filter If we supply the ```filter``` key, its value is used instead of the default: ```js let nodes = [ { op: "filter", filter: "sepia(0.7)" } ]; ``` The generated operation will have sepia filter. ```js new Image("url") .set({ filtera: { op: { name: "filter", filter: "sepia(0.7)" } } }); ``` --- # Gradient Path: /docs/nodes/gradient # Gradient To add gradient to an element, use the following object: ## Default gradient To use the default gradient, we use the ```gradient``` op. ```js let nodes = [ { op: "gradient" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` This generates default ```gradient``` operation on each element. ```js new Text("Gradient").set({ gradient: { op: { name: "gradient", gradient: "linear-gradient(#3498db,#1abc9c)" } } }); ``` ## Custom gradient We can also specify our own gradient. ```js let nodes = [ { op: "gradient", gradient: "linear-gradient(orange, green)" } ]; ``` This generates our custom gradient. ```js new Text("Gradient").set({ gradient: { op: { name: "gradient", gradient: "linear-gradient(orange, green)" } } }); ``` --- # Layout Path: /docs/nodes/layout # Layout To quickly generate common layout variation, we can use the following: You can use operation like this: ```js let nodes = [ { op: { name: "layout", value: "img-leftof-text" } } ]; ``` Then create ```elements``` array with ```free``` element ```js let elements = [ { type: "free" } ]; ``` ## Layout values Layout values allow you to specify positioning of the elements using simple predefined values: ### ```text-above-image``` * text is place above the image ### ```image-overlay-text``` * image is overlayed by text ### ```image-leftof-text``` * image is put left of the text NOTE: ```Image``` and ```Text``` elements are generated automatically. --- # LinkStyle Path: /docs/nodes/linkStyle # LinkStyle Link style operation allows you to style links (`````` elements.). ### 1 - elements ```js let elements = [ { type: "a", text: "nasa", id: "#myLink", block: true, link: "https://www.nasa.gov", color: "white", background: "green", width: "120px" } ] ``` ### 2 - operations You can use the ```linkStyleOperation```. ```js let nodes = [ { op: { name: "link-style", ops: "full" }, target: ["#myLink"] } ]; ``` Here are possible styles: * ```full``` - full link style * ```air``` - link style with border and no background * ```round``` - round style You can combine the styling as you wish: * ```air-round``` - rounded link style with border and no background * ```full-hover``` - full link style with a hover effect * ```air-round-hover``` - rounded link style with border, no background and hover effect This results in ```hover``` and/or```borderObj``` properties being added to respective elements. You can customize them as you wish. ```js new Link() .set({ // ... rest of the properties borderObj: { width: "3px", radius: "0.7rem", color: "#1abc9c" }, hover: { color: "#1abc9c", background: "orange" } }) .render("#mount"); ``` --- # Nodes Path: /docs/nodes/nodes # Nodes Nodes allow you to edit design of the elements. The following nodes are available: ### [animation](animation.md) ### [filter](filter.md) ### [linkStyle](linkStyle.md) ### [blast](blast.md) ### [gradient](gradient.md) ### [namer](namer.sh) ### [span](span.md) ### [copy](copy.md) ### [layout](layout.md) ### [transform](transform.md) ## Availability These nodes are available when used with the following elements: * ```Text``` * ```Wrap``` * ```Link``` * ```FlexRow``` * ```Table``` * ```Code``` * ```Circle``` * ```Polygon``` ## Exceptions ```Span``` node is available only on ```Text```. ```Layout``` node is only available on ```layout``` element. ```LinkStyle``` node is only available on ```Link``` element. --- # Shadow Path: /docs/nodes/shadow # Shadow To use the default shadow, we use the ```shadow``` op. ## Default shadow To add shadow to an element, use the following object: ```js let nodes = [ { op: "shadow" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` This generates default ```shadow``` operation on each element. ```js new Text("Hello").set({ size: "S1", font: "Arial", shadow: { op: { name: "shadow", steps: 3, colors: [ "orange", "green", "yellow" ] } } }).render("#mount"); ``` ## Custom shadow We can also create simpler shadow on an element. ```js new Text("Single shadow").set({ size: "S1", font: "Arial", shadow: { op: { name: "shadow", steps: 1, colors: ["#1abc9c"] } } }).render("#mount"); ``` You can also specify the following shadow properties: * ```movements``` – array with horizontal and vertical movement of the shadow (offsetX is movements[0] and offsetY is movements[1]) * ```radius``` – blur amount of the shadow ```js new Text("Shadow with movements").set({ size: "S1", font: "Arial", shadow: { op: { name: "shadow", steps: 1, colors: ["#1abc9c"], movements: ["16px", "16px"], radius: "6px" } } }).render("#mount"); ``` --- # Span Path: /docs/nodes/span # Span Span is a part of text that can have different contents and design from the rest of your text. To add span, use the span operation. ## Default span To use the create span, we use this node: ```js let nodes = [ { op: "span" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` This generates defaults ```span``` operation on each element. ### Span operation: * ```parts``` - array of objects with the following structure: ### Object structure in parts array * ```text``` - text of the span * ```style``` - Style object with properties. You can use properties as you are used to from the ```set``` method ```js let text = new Text("The first time we went to the Moon.").set({ fluidc: "S1", color: "green", span: { range: ["900px", "1000px"], op: { name: "span", parts: [ { text: "The first time", style: { italic: true, pad: [{l: "3rem"}], stroke: { range: ["950px", "1000px"], op: { name: "blast", background: ["header", "button"], color: "yellow", width: "1px" } } } }, { text: "We went to the Moon", style: {} } ] } } }).render("#mount"); ``` --- # Transform Path: /docs/nodes/transform # Transform ## Default transform To use the filter, we use this node: ```js let nodes = [ { op: "transform" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` This generates defaults ```transform``` operation on each element. ```js new Text("Hello").set({ size: "S1", transform: { values: [ "ty:-20px", "scale(3)", "rotate(30deg)", "skew(40deg, 0deg)", "perspective(500px)", "matrix(1, 0, 0, 1, 50, 50)" ] } }); ``` ## Stopping animation If you want the element to just transform immediatelly without animation, you can use ```static: true``` property. ```js new Text("Hello").set({ size: "S1", transform: { static: true, values: [ "ty:-20px", "scale(3)", "rotate(30deg)", "skew(40deg, 0deg)", "perspective(500px)", "matrix(1, 0, 0, 1, 50, 50)" ] } }); ``` ## Keeping animation in stopped state If you want the element to stay in deformed state after animation is over, you can use ```keep: true``` property. ```js new Text("Hello").set({ size: "S1", transform: { keep: true, values: [ "ty:-20px", "scale(3)", "rotate(30deg)", "skew(40deg, 0deg)", "perspective(500px)", "matrix(1, 0, 0, 1, 50, 50)" ] } }); ``` ## Customizing You can also customize the animation using: ### Style Customize the style of animation using one of the following properties. After the code is generated, you can fully customize the transform. - TORNADO-SWITCH - CUBE-SPIN - LASER-SWEEP - BUBBLE-POP - VORTEX-SINK - SHOCKWAVE ### Duration Set the duration of the animation: * FAST * SLOW * MEDIUM You can see the result of using the ```LASER-SWEEP``` transform ```MEDIUM``` duration in this example: ```js let nodes = [ { op: "transform", style: "LASER-SWEEP", duration: "MEDIUM" } ]; let elements = [ { type: "h1", text: "Hello" } ]; ``` This yields the following result: ```js ... new Text("Hello").set({ //... transform: { op: { name: "transform", transform: { values: [ "ty:-20px", "scale(3)", "rotate(30deg)", "skew(40deg, 0deg)", "perspective(500px)", "matrix(1, 0, 0, 1, 50, 50)" ], duration: "6s-ease-in-out" } } } }) ... ``` ## Explaining the properties You can use the following properties and their CSS equivalent. | Transformation | CSS Equivalent | |----------------------|--------------------------------------| | `ty:-20px` | `transform: translateY(-20px);` | | `scale(3)` | `transform: scale(3);` | | `rotate(30deg)` | `transform: rotate(30deg);` | | `skew(40deg, 0deg)` | `transform: skew(40deg, 0deg);` | | `perspective(500px)`| `perspective: 500px;` | | `matrix(1, 0, 0, 1, 50, 50)` | `transform: matrix(1, 0, 0, 1, 50, 50);` | --- # Agent surface – the page, as tools Path: /docs/raster/agent-surface # Agent surface – the page, as tools An `agent-surface` node lets an AI agent operate the page: move between morph views, submit a form you have allowed, and read what is on screen. Nothing is authored twice – the tools are **derived** from the same `(E, N)` pair that renders the page. ```js let nodes = [ { op: "morph", chain: [ { from: "home", to: { Work: "work" } }, { from: "work", to: { Aurora: "aurora" } } ] }, { op: "agent-surface", name: "studio", forms: ["contact-form"] }, ]; ``` That derives four tools: `studio_navigate`, `studio_go_back`, `studio_submit_contact-form` and `studio_read_view`. ## Why derive rather than annotate Your page already says what it can do. The morph chain *is* the navigation graph, and a form's fields already declare their types and which are required. Writing tool handlers beside all that would restate it in a second place, where it would drift the first time either changed. ## Opt-in, always No node, no surface. Turning a page's interaction structure into callable tools is the page's decision, and upgrading the library never makes it for you. The same goes for forms: **no form is exposed unless you name it in `forms`**, because a derived submit tool is an agent *acting* – the one derived capability with a real side effect. ## What each tool does **`navigate`** takes a `destination` from an enum of the views your graph can reach. These are in-page views reached by a transition, not separate URLs – Nodality has no router, and between pages an agent already has links. Execution goes through the controller's own `goToState`, so an agent's traversal is the user's traversal and inherits the same rules. **`go_back`** is present only where an edge declares `back`. It unwinds the path actually taken. **`submit_
`** fills the real fields and submits through the form's own path, so your listeners and validation attributes apply. Its schema is read from your field descriptors: `inputType: "email"` becomes a string with an email format, a `picker`'s `items` become an enum, a `checkbox` becomes a boolean, and `required: true` becomes a required property. **`read_view`** returns the content currently on screen as structured text – headings, body copy, and the labels of anything actionable. ## Errors are repair reports A destination that exists but is not reachable from where the graph currently stands comes back as a report naming what *is* reachable: ```json { "ok": false, "code": "UNREACHABLE_FROM_HERE", "got": "aurora", "valid": ["work", "contact"] } ``` A submit missing a required field names the field rather than returning a bare failure. An agent repairs from either in one turn. ## The static half Prerendering writes the same declaration into the page as ` ``` Two things in there are easy to get wrong, and both cost a silent no-op rather than an error. The **keys of `to` are the labels the nav actually renders**, not names you choose. `{ type: "nav" }` renders About, Services and Contact, so those are the keys. Change the nav and the keys change with it; use a label that is not on screen and that edge simply never fires. If you would rather not depend on the text, address the links by position instead: `to: ["about", "offer", "contact"]`. The **back button is an ordinary element inside the view**, not a feature you switch on. `back: true` on the node gives the controller a history to unwind; a control has to exist for someone to unwind it with. ## `back` is history, not an edge With `back: true` the destination's own button returns to wherever you came from – the reverse fold over the path actually traversed, not a lookup in the edge list. From `aurora` the graph above offers a forward route to `contact`, but back returns to `work`, because that is the way you came. It is the rule every browser has already taught your users. The state pointer moves only when a transition **completes**. A morph interrupted half way has not arrived anywhere, so a second trigger restarts from the state actually departed from. ## Fields | Field | Meaning | |---|---| | `from` | id of the element that morphs | | `to` | `{ "Label": "id" }` or `["id", …]` | | `chain` | array of `{ from, to, … }` edges; replaces `from`/`to` | | `effect` | a transition preset name, or an inline array of raster nodes | | `duration` | milliseconds; default 900 | | `back` | wire the destination's control to return; default `true` | | `live` | use the HTML-in-Canvas backend; default `false` | | `fade` | how the two captures are combined; default `"morph"` | Every one of these may also be written per edge inside `chain`. ## Backends By default a morph uses the **snapshot** backend: both ends are captured as stills and the shader samples the two. A transition gains nothing from live upload while paying its costs, so `live: true` is an explicit opt-in – and it is a request, not a guarantee. Where HTML-in-Canvas is unavailable the pipeline falls back to snapshot on its own and the page behaves as it did before. ## Validating before you ship `validate_nodes` (see the tool server) checks a morph the way it checks everything else: unknown fields, per-edge typos, ids that resolve to nothing. `duraton: 900` is otherwise a transition that silently runs at the default speed and reports nothing. --- # Raster ops Path: /docs/raster/ops # Raster ops Every node has an `op` and a set of options. Options that are not given fall back to the op's own defaults, so `{ op: "halftone" }` is valid on its own. The full list is available at runtime: ```js import { RASTER_OP_NAMES } from "nodality"; ``` ## Every op at a glance This table is generated from the library source on each publish, so it cannot drift from the code the way a hand-kept list does. The sections below explain what the options mean. | op | stage | options | |---|---|---| | `aberration` | displace | `amount`, `radius` | | `blobs` | color | `alpha`, `color`, `count`, `dispersion`, `frost`, `iridescence`, `radius`, `refract`, `rim`, `trail` | | `copy` | color | `amount`, `count`, `fade`, `points`, `radius`, `rotate`, `scale` | | `dither` | color | `amount`, `ink`, `levels`, `mono`, `paper`, `radius`, `size` | | `duotone` | color | `colors`, `radius` | | `echo` | color | `decay`, `strength`, `tint`, `tintAmount` | | `edges` | color | `color`, `strength` | | `flow` | warp | `radius`, `scale`, `seed`, `speed`, `strength` | | `halftone` | color | `amount`, `angle`, `ink`, `paper`, `radius`, `size`, `softness` | | `hexalize` | cell | `lift`, `radius`, `size` | | `mask` | field | `at`, `from`, `invert`, `radius`, `remap` | | `merge` | color | `mix` | | `noise` | field | `amount`, `remap`, `scale`, `speed` | | `offset` | warp | `radius`, `strength` | | `particles` | displace | `amount`, `reach`, `seed`, `size` | | `stir` | warp + color | `curl`, `decay`, `dye`, `dyeAmount`, `dyeFade`, `dyeResolution`, `force`, `intensity`, `pressure`, `pressureIterations`, `radius`, `rainbow`, `resolution`, `sheen`, `strength`, `tint` | 16 ops, plus `switch`. Drivers: `static` `mouse` `hover` `scroll` `time`. Generated from nodality 1.4.0. ## Colour **`duotone`** — maps luminance onto two colours. ```js { op: "duotone", colors: ["#0b1b2b", "#e6e9ed"], radius: 180 } ``` **`halftone`** — a rotated dot screen. ```js { op: "halftone", size: 5, angle: 15, ink: "#111", paper: "#fff", softness: 0.4 } ``` **`dither`** — ordered (Bayer) dithering. `levels` sets the number of steps per channel; `mono: true` reduces to a single ink. ```js { op: "dither", levels: 4, mono: true, ink: "#0b1b2b", paper: "#fff" } ``` **`edges`** — edge detection. ```js { op: "edges", strength: 1.0, color: "#fff" } ``` **`echo`** — trailing ghosts of the previous frames. ```js { op: "echo", decay: 0.9, strength: 0.6, tint: "#2ad", tintAmount: 0.3 } ``` **`blobs`** — refractive blobs drawn over the content. ```js { op: "blobs", count: 3, radius: 40, refract: 0.6, rim: 0.4, iridescence: 0.3, frost: 0.2, alpha: 0.9, trail: 0.5 } ``` **`copy`** — repeat the content in a point set or ring. ```js { op: "copy", count: 6, radius: 120, rotate: 0.5, scale: 0.8, fade: 0.6 } ``` ## Warp and displace **`offset`** — pushes pixels away from the driver position. ```js { op: "offset", strength: 10, by: "mouse", radius: 180 } ``` **`stir`** — a stable-fluids solver. The heaviest op by far, and the one with the most knobs. `resolution` and `pressureIterations` trade cost against quality; `dye` and `rainbow` add colour carried by the flow. ```js { op: "stir", force: 0.6, intensity: 0.4, curl: 12, decay: 0.98, resolution: 128, pressureIterations: 20, radius: 160 } ``` **`aberration`** — splits the channels. ```js { op: "aberration", amount: 8, radius: 200 } ``` ## Cell **`hexalize`** — quantises into a hexagonal grid. With `lift`, cells near the driver come toward the viewer instead of deforming. ```js { op: "hexalize", size: 14, lift: 0.35, by: "mouse", radius: 180 } ``` Without `lift` the edge is drawn flat, which is what you want if the grid should be invisible until the pointer arrives. ## Fields `mask` and `noise` draw nothing. They write a scalar field for other ops to read — see [Composition](./composition.md). ```js { op: "mask", from: "radial", at: [0.5, 0.5], radius: 220, invert: false } { op: "noise", scale: 3, speed: 0.2, amount: 0.5 } ``` `from` accepts `radial` (the default), `vertical` or `horizontal`. `at` is fractional — `[0, 0]` is the top-left, `[1, 1]` the bottom-right. Left out, a radial mask follows the driver, which itself defaults to the centre. ## Options every node accepts | option | meaning | |---|---| | `by` | driver: `static` `mouse` `hover` `scroll` `time` | | `radius` | falloff distance in pixels around the driver's position | | `as` | name the field this node writes (default `"mask"`) | | `masked` | read a field: `true` for `"mask"`, or a name | | `live` | `false` opts the node out of the HTML-in-Canvas backend | The driver supplies a position and an amount that fade with `radius`, so `by` and `radius` are normally set together. `by: "static"` means the effect is applied evenly and never animates. ## Adding your own ```js import { registerRasterOp } from "nodality"; registerRasterOp("pixelate", { stage, decl, code, uniforms }); ``` `stage` decides where the code runs in the chain; `decl` declares uniforms, `code` emits GLSL, and `uniforms` uploads values each frame. --- # Basics Path: /docs/scrollanimation/basics # Basics To create smooth scroll animations you can use the following: * ```ScrollVideo``` - play video as you scroll * ```TransformAnimation``` - transform object as you scroll * ```Stacker``` - element to combine these sections You can combine ```ScrollVideo``` and ```TransformAnimation``` elements insie the ```Stacker``` element to crate engaging scroll effects. :::tip Note: You don't create these classes using objects inside ```elements``` array, but you need to create them directly. Here are the necessary imports. The [following](scrollvideo.md) pages will show you how to use these classes. ::: ```js import { KeyframeAnim } from "../lib/appleAnim.js"; import { TransformAnim } from "../lib/transformanim.js"; import { Stacker } from "../lib/stacker.js"; import { ScrollVideo } from "../lib/scrollvideo.js"; ``` --- # Scroll Animation Path: /docs/scrollanimation/index # Scroll Animation Create engaging scroll animation Continue to [Basics](basics.md) to get started. ### [ScrollVideo](scrollvideo.md) - play video as you scroll ### [TransformAnimation](transformanimation.md) - transform object as you scroll ### [Stacker](together.md) - element to combine these sections --- # ScrollVideo Path: /docs/scrollanimation/scrollvideo # ScrollVideo Play video as you scroll. ## Arguments * ```minScrollHeight``` - defines at what scroll height the playback should start * ```maxScrollHeight``` - defines at what scroll height the playback should stop * ```videoURL``` - URL of video to be played ```js new ScrollVideo().set({ minScrollHeight: 0, maxScrollHeight: 1500, videoUrl: "https://www.apple.com/media/us/mac-pro/2013/16C1b6b5-1d91-4fef-891e-ff2fc1c1bb58/videos/macpro_main_desktop.mp4" }); ``` --- # Together Path: /docs/scrollanimation/together # Together Here is how you can combine the elements using the ```Stacker``` element. ### Step 1 - Basic setup ```html
``` ### Step 2 - Import necessary elements ```html ``` ### Step 2 - Defining elements array To make Text move with page, uncomment ```height: 20000px``` declaration at line 107 ```js let elements = [ new Stack().setup({}).add([ new Text("Alpha").set({ color: "orange", fluidc: "S1", font: "Arial", pad: [{t: "30rem"}], onScroll: { value: "opacity", from: 0, to: 1000, valMin: 0, valMax: 1 }, zIndex: 1 }), new Text("Beta").set({ color: "orange", fluidc: "S1", font: "Arial", pad: [{t: "140rem"}], onScroll: { value: "opacity", from: 100, to: 1100, valMin: 0, valMax: 1 }, zIndex: 1 }), new Text("Gamma").set({ color: "orange", fluidc: "S1", font: "Arial", pad: [{t: "50rem"}], onScroll: { value: "opacity", from: 200, to: 1300, valMin: 0, valMax: 1 }, zIndex: 1 }), new ScrollVideo().set({ minScrollHeight: 0, maxScrollHeight: 1500, videoUrl: 'https://www.apple.com/media/us/mac-pro/2013/16C1b6b5-1d91-4fef-891e-ff2fc1c1bb58/videos/macpro_main_desktop.mp4' }) ]), new TransformAnim({ overflow: "hidden", content: new Wrapper().set({}).add([ new Center().items([ new Image().set({ id: "scaleMe", url: "https://iplanet.one/cdn/shop/files/MacminiM2_performance_m2_large_2x_MacminiM2.png?1673043795288", width: "50%", onScroll: { value: "scale", from: 2600, to: 3200, valMin: 2.4, valMax: 0.67 } }) ]), new Text("Supercharged").set({ color: "green", align: "center", fluidc: "S1", font: "Helvetica" }), new Text("Power to you").set({ align: "center" }) ]) }) ]; ``` ### Step 3 - Defining Stacker Stacker allows you to stack elements for use with scroll animations. Use the ```add``` method to add views. This method will be used to stack them below each other. ```js new Stacker().add([ new Wrapper() .set({}) .add([ elements[0] ]), new Wrapper() .set({ height: "2000px" }) .add([ elements[1] .set({ height: "120vh" }) .toSticky() ]), new Wrapper() .set({ background: "green", height: "auto", pad: [{t: "4900px"}] }) .add([ new Text("Hello").set({ fluidc: "S1", color: "yellow" }), new Text("Below").set({ fluidc: "S1", color: "yellow" }) .set({ pad: [{b: "200px"}] }), new Text("bottom").set({ fluidc: "S1", color: "yellow" }) ]) ]).render("#mount"); ``` --- # TransformAnimation Path: /docs/scrollanimation/transformanimation # TransformAnimation Transform element as you scroll down. Use the ```onScroll``` modifier to decide which property and over what scrollrange will change. ### Parameters - **from** Specifies the starting value of the property. - **to** Specifies the ending value of the property. - **valMin** Defines the minimum range value. - **valMax** Defines the maximum range value. - **value** Determines which property will change. You can use ```opacity``` or ```scale```. The desired property will change between the `valMin` and `valMax` in ```from``` - ```to``` scrollRange. ### Example In this example we use ```scale```, where the elemnt is scaled between ```24100px``` to ```24400px```. ```js new TransformAnim({ path: "IMG/M2.jpeg", title: "Supercharged", overflow: "hidden", content: new Wrapper().set({}).add([ new Center().items([ new Image().set({ id: "scaleMe", url: "image.png", width: "50%", onScroll: { value: "scale", from: 24100, to: 24400, valMin: 2.4, valMax: 0.67 } }) ]) ]) }); ``` --- # Prerender API Path: /docs/ssg/api # Prerender API The CLI covers the common case. When you need to drive rendering from your own build script – a different template per page, a computed output path, content fetched before the render – import `prerender` directly. ```js import { prerender } from "nodality/ssg"; await prerender({ template: "src/index.html", output: "upload/index.html", build: async () => { const elements = [{ type: "h1", text: "Hello" }]; const nodes = [{ op: "blast" }]; new Des() .nodes(nodes) .add(elements) .set({ mount: "#mount" }); } }); ``` `template`, `output` and `build` are required; `build` must be an async function. The rest have defaults. ## Options | Option | Default | Meaning | |---|---|---| | `template` | – | HTML shell to render into. Required | | `output` | – | Where to write the result. Required | | `build` | – | Async function that constructs the page. Required | | `mount` | `"#mount"` | Selector the rendered tree replaces | | `url` | `"http://localhost/"` | Document URL seen by the page | | `viewport` | `{ width: 390, height: 844 }` | Size reported to responsive code | | `locale` | – | Locale for this render | | `localStorageKey` | `"h7lang"` | Key the page reads its locale from | | `htmlLang` | – | `lang` attribute on `` | | `canonical` | – | Canonical URL for this page | | `alternates` | – | `hreflang` alternates | `viewport` matters more than it looks. Responsive code branches on width at build time, so a page prerendered at the default 390px ships its mobile layout as the pre-JavaScript HTML. Set it to match the layout you want crawlers and the first paint to see. ## Multiple locales `prerenderEachLocale` renders the same page once per locale, with the locale set before `build` runs. ```js import { prerenderEachLocale } from "nodality/ssg"; ``` ## Whole sites `prerenderSite` takes one config and renders every page, in every locale, and writes a sitemap. ```js import { prerenderSite } from "nodality/ssg-site"; await prerenderSite({ origin: "https://example.com", uploadDir: "upload", pages: [ { html: "index.html", entry: "pages/index.js" }, { html: "about.html", entry: "pages/about.js" } ], locales: ["en", "cs"], defaultLocale: "en" }); ``` `origin`, `uploadDir` and `pages` are required, and every page needs both `html` and `entry`. Omit `locales` and it renders a single unlocalised pass. Each locale runs in its own subprocess. That is deliberate: a page's module-level state – a cached translation table, a memoised layout – would otherwise leak from one locale's render into the next, and the second language would come out subtly wrong in ways that only show up in production. ## Node caveat The browser bundle touches `window` when it is imported, so `require("nodality")` throws in bare Node. The `ssg` entry points are built for a build script and install their own DOM; if you want to import the library itself outside a browser, provide a DOM first. --- # Prerendering Path: /docs/ssg/prerender # Prerendering Nodality builds your page in the browser. That is fine for an app behind a login, and a problem for anything that needs to be indexed or to paint before JavaScript runs: the HTML a crawler receives is an empty `
`. Prerendering runs your page once at build time, in a headless DOM, and writes the resulting markup back into the HTML file. The shipped page then contains real content, and Nodality still boots on top of it in the browser. ```bash npx nodality prerender ``` ## What it does It reads the HTML files in your upload directory, executes the page's own entry script against a jsdom document, and replaces the mount point's contents with the rendered markup – in place, in the same file. Nothing about how you write the page changes. The same `elements` and `nodes` arrays produce the page at build time and hydrate it at runtime. ## Install jsdom `jsdom` is a peer dependency, needed only at build time. It is not bundled, because a browser-only consumer should not pay for it: ```bash npm install --save-dev jsdom ``` If it is missing, the command fails with an explicit message rather than the default `Cannot find package 'jsdom'` stack trace. ## Configuration Flags are optional. Anything not passed falls back to `nodality.config.json` in the project root, then to the defaults. ```json { "origin": "https://relays.app", "uploadDir": "upload", "tolerateAsyncErrors": false } ``` | Key | Flag | Meaning | |---|---|---| | `origin` | `--origin=` | Absolute site origin, used for canonical URLs and the sitemap | | `uploadDir` | `--upload=` | Directory holding the HTML to render. Default `upload` | | – | `--out=` | Write elsewhere instead of in place | | – | `--tolerate-async` | Do not fail the build when an async task is still pending | | – | `--verbose` | Report each page as it renders | | `locales` | `--locales=` | Render one copy per locale | | `defaultLocale` | `--default-locale=` | Which locale is served at the root | ```bash npx nodality prerender --origin=https://example.com --tolerate-async npx nodality prerender --verbose ``` ## Async content A page that fetches data has not finished rendering when the synchronous pass ends. The prerender waits for pending work, and by default **fails** if something is still outstanding – a page that silently prerendered half its content would ship looking fine and be wrong only for crawlers, which is the hardest kind of bug to notice. `--tolerate-async` downgrades that to a warning when you know the outstanding work is not part of the initial paint. ## Other commands ```bash npx nodality compile [src/file.js] # emit Designer output as a companion file npx nodality fanout # generate per-item pages from JSON npx nodality stage [--upload=DIR] # copy the installed ESM bundle into upload/ npx nodality help ``` `stage` is the one consumers reach for most after `prerender`: it copies `dist/index.esm.js` into your upload directory and stamps the installed version into `_nodality-version.js`, so a deploy cannot ship page code and library code from different releases. It writes only when the content actually changes, so it is safe to re-run.