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.
| Import | What it does |
|---|---|
@getv.design/embed/static | A document in, an SVG string out. No DOM: Node 22.3+ and browsers. |
@getv.design/embed/view | Show a document in an element, page through it, present it. |
@getv.design/embed/edit | The vDesign editor, with its floating toolbar. |
@getv.design/embed | mount(el, source, { mode }): loads one of the above on demand. |
@getv.design/embed/view.css, @getv.design/embed/edit.css | The 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.
-
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 allowstyle-src 'unsafe-inline'. -
Linked. Serve the package’s
view.css(andedit.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’sdist/from.style-src: wherever you serveview.cssandedit.cssfrom.img-src blob:for a document’s images, which are drawn from object URLs, anddata:for the editor’s cursors.connect-src: the document URLs you pass, and the font location (below). With the default, that ishttps://cdn.jsdelivr.net.font-srcneeds nothing: the faces are fetched as bytes and given to the page asFontFaces, whichfont-srcdoes 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-accentstill 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
problemsorfontsevent, 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.