graview-view-links
graview-view-links is one of Graview's 31 authoring skills: instructions an AI coding assistant such as Claude Code or Codex reads before it changes a Graview app. What it teaches: Send a person from a Graview worker view to a record, a place or a list — an <a data-record>, or a whole card or row bound to a record that opens on a click, a phone's tap and Enter — narrowed and to a section, and prove each lands by pressing it on a phone as well as a desk rather than assuming a card the describer lists opens.
Installed into a project by graview skills install ., and read by whichever assistant is working beside you.
The skill
Links in a worker view
A worker view (graview-worker-view: its manifest, its global, its limits) writes no address of the page's: an href it built is never drawn. What it may link to is a record, a named place or a kind's list of this app, and the host makes that a link and follows it, on both faces, only to what the viewer may see.
Go somewhere with a link, do something with a button. <a data-record> or <a data-place> to go; <button>, or <button data-act> (graview-view-forms), to do. Style both as you like: the host resets a button and a link it made to the words around them at no specificity, so your own class always wins. A <div> or <span> with a graview.on("click") handler is reached by no keyboard and is nothing to a screen reader: do not draw one. A list of records is a list of <a data-record>, never graview.on("click", "[data-id]", … navigate).
A link
<a data-record="offer:7"> and <a data-place="the-packages"> are links the host follows. On the routed pages the host gives each the address it goes to as its href (/a/<app>/offers/offer%3A7), so it shows on hover, copies, and opens in a new tab on Cmd- or Ctrl-click or a middle click; a plain click, a tap and Enter stay in the app. On the scene, which has no addresses, it is given the keyboard and a link's role instead — and so in an embed whose host keeps its own addresses (memory routing), unless the host says its address for a page with the embed's hrefOf. A <button data-record> goes there too. <a href> goes out only to an address a record was handed — its own url, mailto:, tel: (below). A destination is a record's id, else a place by its slug or its title (data-place="songs-by-writer" or "Songs by writer"), else a kind's list (features or feature); any other says why, naming the places. A titled view of many is a place too, at /places/<its title as a slug>: data-place="live-threats" reaches the view "Live threats", and the check knows it when its host hands the views it keeps. The Scene and the home are places by name as well: data-place="overview" (or the Scene's own title, "The map") goes to the Scene's overview — on the pages its own address, on the Scene everything from above — and data-place="home" to the front page; a lens goes by its slug, data-place="the-board". The check and the page read one vocabulary (placeNamesOf on @graview/core/navigate), so a name the check passes the page follows. From code, graview.navigate("offer:7") and graview.navigate("overview") go by the same rules.
In SVG the same <a data-record> is a link — a timeline's dot, a map's pin — with its address, the keyboard and Enter: <svg role="group" aria-label="…"><a data-record="amendment:19" aria-label="19th, 1920"><circle …/></a></svg>. It takes no href of yours; a link out is an HTML one.
Links out
<a href="${node.url}"> is drawn where the address is one a record the view was shown holds, as it holds it — an https: page, a mailto:, a tel: — or an https: address on an origin the host lists (links: { origins } on the view's definition and a headless run). The host opens it in a new tab, no opener, no referrer, marked ↗. An address you build, javascript:, data: or http: is refused: a link a view made could carry what it reads out.
A card that opens
Any element with data-record or data-place is a link, not only an <a>. The same mark draws a line: at Nearby on the one face, each data-record element in sight has one from the page's edge to that record's building, as a built-in page's rows do — none for an id that is no record or one the seat cannot see — and a view of one record that marks nothing still has its one line, to that record. A data-record element counts as shown even when hidden, so the Connected list does not say it again. Put an <a> to the same record on its heading:
<div class="card" data-record="threat:7">
<h3><a data-record="threat:7">Invoice phishing</a></h3>
<span class="badge">critical</span><span class="badge">new</span>
<p>Forty mailboxes got the fake invoice.</p>
<button data-more="threat:7">More</button>
</div>The heading's <a> is the keyboard's and the screen reader's link; the card, a <div> or an <li> alike, takes no role and no tab stop of its own, and is a bigger target: the host draws it with a pointer, touch-action: manipulation and a faint tap highlight, and follows it on a click and a phone's tap anywhere inside it — its heading, a badge, a line — unless the press lands on something with a press of its own: a button, a field, a data-act, a lightbox, a drop target, a control's role, another link. So "More" above toggles and opens nothing, and the audit finds no link inside a link. A card with no such <a> is given the keyboard and role="link" itself (a list's item keeps its role; a table's row gives the keyboard to its first cell); then it may hold no button, and a heading that links elsewhere is a link inside a link.
It is no link when it is a press scope: a fieldset or role="group" (the record a form's acts are done to), an element with data-act, data-lightbox, data-drop or data-field, a field, or an element that holds a field to type into or pick from (a checkbox inside leaves its row a link). Link such a card's title instead: <h3><a data-record="…">.
Do not work around it: no data-open with graview.on("click", …, graview.navigate), no pointer-events: none on a card's children, no touch-action of your own. A card that should do something of its own on a press carries data-key, not data-record, and graview.on.
How a tap reaches the view
A phone makes a tap a click on what the finger landed on; iOS does so only on what looks pressable, which is why the host draws every link with a pointer. The host hears it on the view's shadow root, follows a link first, then tells the view, which hears click as it hears a mouse's: graview.on("click", ".card", (e) => …) is handed e.target, what the finger landed on, and e.element, the element the selector matched at or above it, so a tap on a card's heading hands the card.
Narrowed, and to a section
A list or a lens may be gone to narrowed: where: { stage: "building" } in navigate, data-filter="stage:building" on a link — the words the list's own arranging line writes, so the address is /features?filter= stage%3Abuilding and can be shared. A lens narrows the records it is handed; a blocks lens lists what its own where says. Any destination may take a section, a heading's words or an element's id (section: "Every feature", data-section): the page scrolls there and gives it the keyboard (#every-feature). On the scene a list is the kind's stop narrowed (in.filter); a section is looked for there too. A filter on a field the kind lacks, or a choice it lacks, goes nowhere and says why (Features have no field "stag" to filter by; they filter by …), so link to the list narrowed rather than to a lens per filter; a lens kept only as a link target can be hidden (set-lens-hidden, graview-pages).
A jump, what is in sight, and a file
<a href="#eras"> jumps to an element of the view's own: the host scrolls it to the top, below its own sticky bar, gives it the keyboard and leaves the address alone. A scroll margin on it (scroll-margin-top) clears a sticky header you draw. The elements jumps name, and any with data-sight, are watched as the page scrolls: the one in sight is data-in-sight, its jumps aria-current="location", and the view hears sight on it (graview.on("sight", "section", …)). <a data-file="${node.id}" data-field="contract"> opens a record's file in a tab of its own; data-download downloads it under its own name.
A column the menu knows
Mark a region that holds one value of a field data-column="status:doing" (and data-column-kind="task" where the place is not that kind's; a heading's words in data-column-label). A right-click there, as on a status board's column, offers "Add a task in Doing" — the add act with status given — and "Show only Doing", the place narrowed as above.
Then find out whether it worked
graview checkthe app the view links into: a link is only as sound as the records and places it names.- `graview view check view.js --app <app> --manifest manifest.json --seed <seed.json> --roles <role>
says everydata-recordordata-place` that names nothing the seat was shown and no place, and every filter that goes nowhere, and presses each link, jump and file link: one that goes nowhere fails.--press ".card h3"presses there and says where the host would go, or that the press is a button's of its own.--auditholds what it drew to the keyboard and screen-reader rules, as the host draws it: a<div>heard only bygraview.on("click"), a control or link with no name, a toggle with no state, a button inside a card that opens, an<img>with no alt. Each comes with its WCAG criterion and a one-line fix, and the check fails on what the view must fix. Contrast, the ring and a keyboard trap need a browser: add--browser(the project's Playwright and its Chromium) to run them. - Open it on a phone, 390 pixels wide with touch: tap a card's heading and a badge inside it, and see the record's page; tap a toggle inside it, and see it toggle and stay. On a desk, Tab through the view: every link and button is reached, with a ring; Enter on a card opens it, and Cmd-click on an
<a data-record>opens it in a new tab. - Follow each link narrowed and to a section, and see it land where it says.
What the check cannot see
graview check judges the declaration, not the view. A headless run lays nothing out, so it cannot see a card covered by something drawn over it, a link too small to tap, or a card that looks like it opens and holds a field. Only tapping it on a phone does. Nor can it see a link that goes somewhere true but unhelpful.