Machine mode

Render a cascivo UI as a Markdown document — no CSS, no hydration, no interactivity. One package, @cascivo/text, with three entry points depending on where the UI is.

pnpm add @cascivo/text

The one rule

Machine mode is the accessibility tree, serialized. Every decision the serializer makes reads what a screen reader reads: semantic elements, ARIA roles and states, accessible names.

That is not a stylistic choice, it is the only signal available. cascivo ships CSS Modules, so class names are build-time hashes (_badge_1r5fv_83) that mean nothing and change on every build. The accessible layer is the one part of a component's output that is specified, tested (apg:check, rtl:check, the enhancement-renders sweep) and covered by semver.

Two consequences follow, and both are deliberate:

Server or agent: HTML in, Markdown out

import { renderToStaticMarkup } from 'react-dom/server'
import { toMarkdown } from '@cascivo/text'

const doc = toMarkdown(renderToStaticMarkup(<Dashboard />))

@cascivo/react's node export condition resolves to the CSS-free twin, so this runs in a bare Node process with no bundler and no stylesheet.

In the browser: the live DOM, with what people have actually done to it

import { elementToMarkdown } from '@cascivo/text'

const doc = elementToMarkdown(document.querySelector('main')!)

This reads control properties, not attributes — the value someone typed, the box they checked, the disclosure they opened. An HTML string only ever carries the state the UI was rendered with.

As a component

import { TextView } from '@cascivo/text/react'

<TextView>
  <Dashboard />
</TextView>

The children render into a container that is display:none, inert and aria-hidden, and the Markdown is shown instead. They render at all because that is where the state lives — a <TextView> that serialized React elements would know what a form was given, never what someone typed into it. There is one DOM tree, not two: no duplicated useId() values, no second copy of the page competing for a screen reader's attention.

The document follows the live DOM, so it tracks typing, toggling and structural changes.

To show the UI and its text — the side-by-side on cascivo.com — use the hook <TextView> is built on, against a container of your own:

import { useLiveMarkdown } from '@cascivo/text/react'

const stage = useRef<HTMLDivElement>(null)
const doc = useLiveMarkdown(stage)

<div ref={stage}><Billing /></div>
<pre>{doc.value}</pre>

From a ViewConfig

The JSON view runtime exposes viewToMarkdown(config, { data }) on its /text subpath, which turns a ViewConfig into the same document its <CascivoView> would produce on screen. It renders the real components and serializes their output rather than walking ComponentNodes, so it cannot drift from what the components actually render.

It sits on a subpath rather than the root export because it renders to a string, and a root export that reaches react-dom/server would pull a server renderer into every browser app that imports the view runtime — eight of them here.

That runtime is internal to the monorepo today and is not published to npm, so this entry point is available to the docs, MCP and playground surfaces rather than to an installed app. The other three work anywhere.

What the document looks like

Affordances are named in one grammar — [kind: name = value (state)]:

# Billing

[nav: Breadcrumb]

1. [Home](/)
2. Billing

Email [input = "[email protected]"]
[checkbox: Remember me = checked]
[select: Plan = Pro]

[tab: Overview (selected)]
[tab: Usage]

| Name | Plan |
| --- | --- |
| Ada | Pro |

[button: Save changes] [button: Cancel (disabled)]

Set annotate: false for a pure reading document: affordances drop out, a button keeps its label (those words are part of the page), a text field contributes nothing.

What is kept, and what is dropped

KeptDropped
Visually-hidden (sr-only) content — this is where a chart's data table livesaria-hidden, inert, hidden, display:none subtrees
Collapsed disclosures, closed menus and dialogs, annotated with their state<script>, <style>, <canvas> (decorative by contract)
ARIA states that a reader needs: disabled, selected, expanded, current, invaliddata-state at rest: idle, default, active, inactive, on, off
data-state values CSS pseudo-classes cannot express: loading, error, openDecorative images (alt="") and unnamed SVG

Collapsed content is expanded on purpose. A human can click to reveal; a document cannot, and a reader that silently drops half a page is worse than one that says which part was collapsed.

What it will not claim

Modal opens by calling showModal(), so an open modal's server HTML carries no open attribute — nothing distinguishes it from a closed one. Machine mode prints no state there rather than printing (closed): a false statement about the UI is worse than a missing one, because an agent cannot tell it from a true one. In the browser the property is readable, so elementToMarkdown and <TextView> do report it.

Options

OptionDefaultWhat it does
annotatetrueName the affordances and their state.
links'inline''inline' keeps standard Markdown links; 'footnote' numbers them and lists the URLs at the end; 'strip' keeps the label only.
width0Wrap column for plain paragraphs. Tables, fenced code and lists are never wrapped.

Limits

As Markdown: /docs/machine-mode.md

← All guides