Open app
Contents Embedding

These docs follow the latest development version of vDesign.

Embedding

@getv.design/embed shows vDesign documents in any web page: as an SVG, in a view the reader can page through and present, or in the vDesign editor. It works with any framework or none, and is free to use, including commercially, under its own licence.

That is the view, on this page: page through it, zoom with ⌘ or Ctrl and the wheel, and present it. To draw designs in Markdown, see Designs in Markdown.

Every function, option and event is in the API reference. The package is at 0.x, so an API may still change in a minor version.

Install

From npm, for a bundler:

npm install @getv.design/embed
import { view } from '@getv.design/embed/view';

const text = '…'; // a .vd document, as text; or pass a URL or a Blob
const handle = await view(document.querySelector('#design'), text);

Without a bundler, import the ES modules from jsDelivr. Import the entry file itself, not jsDelivr’s +esm rebundling, so the editor and the font code still load only when needed:

<div id="design"></div>
<script type="module">
  import { view } from 'https://cdn.jsdelivr.net/npm/@getv.design/embed@0.2.0/dist/view.js';

  const text = await (await fetch('/designs/home.vd')).text();
  await view(document.querySelector('#design'), text);
</script>

Give the element a size in your stylesheet: the view fills it.

ImportWhat it does
@getv.design/embed/staticA document in, an SVG string out. No DOM: Node 22.3+ and browsers.
@getv.design/embed/viewShow a document in an element, page through it, present it.
@getv.design/embed/editThe vDesign editor, with its floating toolbar.
@getv.design/embedmount(el, source, { mode }): loads one of the above on demand.
@getv.design/embed/view.css, @getv.design/embed/edit.cssThe styles, for a strict content security policy.
@getv.design/embed/fonts/*The Inter faces, to serve yourself.

Each entry loads only what it needs. A page that views a document never downloads the editor (about 85 KB gzip), and the code that lays text out from font files (about 150 KB gzip) loads after the first paint.

Draw an SVG

import { parse, renderSvg } from '@getv.design/embed/static';

const { document, problems } = parse(text); // never throws; problems lists what is wrong
const svg = await renderSvg(document); // or renderSvg(text), a URL, or a Blob
const second = await renderSvg(document, { page: 1 }); // one page, by 0-based index or id

By default text is measured approximately, without font files, so a line can break slightly differently from the app. With fonts: 'exact', text is laid out from the font files the app exports with, and lines break where the app’s SVG export breaks them. In Node the files are read from the package’s own fonts/ folder; in a browser they are fetched from fontBase. If a face cannot be read, the SVG is drawn approximately and onProblems hears a fonts-unavailable warning; renderSvg does not reject for it.

View and present

import { view } from '@getv.design/embed/view';

const handle = await view(el, text, { fit: 'page', page: 0 });
handle.on('problems', (problems) => console.log(problems)); // called at once with the current list
handle.goTo('pricing'); // a page by id or 0-based index
await handle.load(newText); // redraws in place; unchanged parts are not rebuilt
const play = await handle.play(); // present over the view; Escape comes back
handle.destroy(); // el is left empty, and image URLs are released

The view adds one <div> inside el and fills it. A page is a top-level frame that is not hidden.

The view never takes the reader’s scroll. A plain mouse wheel over it scrolls your page as usual; Ctrl or ⌘ with the wheel, or a trackpad pinch, zooms, and middle-drag or Space-drag pans. interactive: false turns all of that off. A link in the document opens in a new tab, or goes to onLink if you give one.

Edit

import { edit } from '@getv.design/embed/edit';

const editor = await edit(el, text, { fit: 'page' }); // the same sources and options as view()
editor.on('change', (event) => save(event.text())); // once per undo step; text() is the whole .vd file
editor.undo();
editor.redo();
const handle = await editor.view(); // back to a view, where the person was looking

From a view, await handle.edit() swaps the view for an editor in the same element and keeps the camera; mount(el, source, { mode: 'edit' }) opens straight into one.

change fires once for each step undo would take back: a whole drag is one, when the pointer comes up. A picture pasted or dropped onto the canvas goes into the document itself, so getText() carries it and nothing is uploaded anywhere. toolbar: false leaves out the toolbar and the context menu, for a page with its own controls.

The editor scrolls and zooms as the view does. A plain click selects, so a link is followed on ⌘-click (Ctrl elsewhere). Its chrome is drawn for the default dark surface; with a light one, also set --vd-page-label and --vd-page-label-hover on el, the colours of each page’s name above its corner.

Styles and content security policy

The embed’s styles reach the page by one of two routes.

  1. Injected, the default. The view inserts one <style id="vd-viewer-styles"> into the page’s head, and the editor another, <style id="vd-editor-styles">. Nothing to set up, but a content security policy must allow style-src 'unsafe-inline'.

  2. Linked. Serve the package’s view.css (and edit.css, if you edit) and link them under those ids before the embed runs. The embed finds the id and inserts nothing, so the page needs no 'unsafe-inline':

    <link rel="stylesheet" id="vd-viewer-styles" href="/view.css" />
    <link rel="stylesheet" id="vd-editor-styles" href="/edit.css" />
    

The embed runs, every mode of it, under this policy once the styles are linked, with the package served from the page’s own origin:

default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
  • script-src: wherever you serve the package’s dist/ from.
  • style-src: wherever you serve view.css and edit.css from.
  • img-src blob: for a document’s images, which are drawn from object URLs, and data: for the editor’s cursors.
  • connect-src: the document URLs you pass, and the font location (below). With the default, that is https://cdn.jsdelivr.net.
  • font-src needs nothing: the faces are fetched as bytes and given to the page as FontFaces, which font-src does not govern.

Served from jsDelivr, the same policy needs https://cdn.jsdelivr.net in script-src, style-src and connect-src.

An SVG from renderSvg carries style="mix-blend-mode: …" on a layer with a blend mode. Inlined into a page under style-src 'self', that layer draws without its blend mode; shown as an <img>, the SVG is unaffected.

The theme is a set of CSS custom properties you can set on the element you give the embed; --vd-accent, for example, colours the selection.

Fonts

By default (fonts: 'exact') the view lays text out from the same font files the app exports with, so lines break where the app breaks them. It draws at once with your page’s fonts, fetches only the faces the document uses (one .woff2 per weight and style), and lays the text out again when they arrive. handle.on('fonts', …) says 'exact' once they are in use. If a face cannot be fetched, the view stays on your page’s fonts, problems gains a fonts-unavailable warning, and nothing throws. fonts: 'browser' skips all of it.

Self-hosting the faces

The faces come from fontBase, by default the package’s fonts/ folder on jsDelivr:

https://cdn.jsdelivr.net/npm/@getv.design/embed@0.2.0/fonts/

So by default your readers’ browsers ask jsDelivr for them. To keep those requests on your own origin, copy node_modules/@getv.design/embed/fonts/ to your site and point fontBase at it:

await view(el, text, { fontBase: '/assets/vdesign-fonts/' });

The faces are Inter, under the SIL Open Font License, in the same folder as OFL.txt.

Your page’s CSS

There is no shadow root, so your page’s CSS reaches the embed. Every class it uses starts with vd-, which keeps its rules and yours from colliding, but a reset that targets elements still applies. Under a reset like a CSS framework’s preflight (svg { display: block }, button { all: unset }, * { color: red }, border-box sizing):

  • The drawing is unaffected. A document’s colours, sizes and fonts are attributes on its SVG, which element selectors do not reach, and --vd-accent still colours the selection.
  • * { color: … } recolours the editor’s chrome. The toolbar’s icons and the context menu’s labels take your colour, and a disabled menu item no longer looks dimmed.
  • button { all: unset } and border-box resets resize the chrome. The toolbar grows, and the context menu’s shortcut column is cut off at its right edge. The view draws no buttons, so only the editor is affected.

If your stylesheet has rules like these, scope them to your own content so they do not match inside the element you give the embed.

Attribution

Not required. The notices in the code are the only credit the licence asks you to keep, and a bundler that moves them into a notices file keeps them well enough. A “Made with vDesign” link to getv.design is appreciated.

Not yet

  • Arrow keys do not move a selected shape in the editor yet; drag it instead.
  • The editor has no problems or fonts event, so a face it could not fetch goes unreported.

Not planned for the first release: shadow-root isolation, the inspector and layers panels, a custom element, React or Vue wrappers, an IIFE or UMD build, and images in Node.