Guides
Framework recipes
The embed is an iframe, so most of these are three lines. What differs between them is where the loader script goes, whether it survives a client-side navigation, and, for hosted CMSes, whether it is allowed to run at all.
Two forms, and when the second is the answer
Everything below is one of two shapes. The loader form is a placeholder plus embed.js, and its only job is to build the iframe and keep its height in sync with the chart. The iframe form is the same chart with no JavaScript of ours anywhere: you set the height yourself and lose nothing else.
<div class="vp-chart" data-symbol="BTCUSDT" data-datafeed="myfeed" data-height="480"></div>
<script src="https://view.verticalprop.com/embed.js" async></script><iframe src="https://view.verticalprop.com/embed?symbol=BTCUSDT&datafeed=host"
style="width:100%;height:480px;border:0" loading="lazy"></iframe><script> tags, which is most hosted CMS editors, most email-adjacent renderers, and every Markdown pipeline with sanitisation on. It is the commonest reason an embed “does not work”, and it is not a bug you can debug from our side.A bare iframe with no JavaScript at all cannot talk to your page, so it cannot supply data or be driven, it is display-only, and on a site that is not ours it is refused for exactly that reason (nothing on the page can answer for the bars).
<script src> from us, the normal answer at an exchange, a broker or anywhere with a security review, you can supply data and drive the chart from a bridge you write and bundle yourself. That is a third form, it is fully supported, and it has its own page: the Wire protocol. embed.js is a convenience wrapper over it, not a dependency.| Form | JavaScript on your page | Whose code | Can supply data / drive |
|---|---|---|---|
embed.js | yes | ours, fetched from our origin | yes |
| Your own bridge | yes | yours | yes |
| Bare iframe | none | – | no, display only |
React
Register the datafeed once, mount the placeholder, and let the loader scan. The important detail is that vpEmbed.datafeed() must be called before a frame asks for bars, registering in an effect that runs on mount is early enough, because the frame cannot request anything until it has loaded.
window.vpEmbed is typed everywhere, with the datafeed's own methods checked as you write them. A hand-written declare global gets you past the compiler, and past the two mistakes worth catching: a bar's time is in milliseconds while a request's from is in seconds, and feed: unknown reports neither.import { useEffect, useRef } from 'react';
import type { VpDatafeed } from '../types/vp-embed'; // the file above
const myFeed: VpDatafeed = { /* resolveSymbol, getBars, … */ };
export function VerticalChart({ symbol }: { symbol: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
// Register the feed BEFORE the loader builds a frame for it.
window.vpEmbed?.datafeed('myfeed', myFeed);
// A placeholder added after the initial scan needs one. This is idempotent,
// so calling it on every mount is safe and is what makes SPA routing work.
window.vpEmbed?.scan();
}, []);
return (
<div
ref={ref}
className="vp-chart"
data-symbol={symbol}
data-datafeed="myfeed"
data-height="480"
/>
);
}Load embed.js once, from your document head or a <script> in the app shell. Loading it per component is harmless, it claims one global and re-scanning is a no-op for placeholders that already have a frame, but it is a request per mount.
data-symbol after the frame exists does nothing; drive it through the JavaScript API instead. Remounting destroys the chart, refetches every bar, and loses whatever the reader had drawn on it.Next.js
Two things differ. The loader is a third-party script, so it belongs in next/script with afterInteractive; and the component holding the placeholder must be a client component, because the datafeed registration touches window.
import Script from 'next/script';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Script src="https://view.verticalprop.com/embed.js" strategy="afterInteractive" />
</body>
</html>
);
}If your page is statically generated, prefer the iframe form and skip the script entirely a static page with a fixed-height frame needs nothing from the loader, and the height handshake is the only thing it gives up.
Vue
<script setup>
import { onMounted } from 'vue';
const props = defineProps({ symbol: String });
onMounted(() => {
window.vpEmbed?.datafeed('myfeed', myFeed);
window.vpEmbed?.scan();
});
</script>
<template>
<div class="vp-chart" :data-symbol="symbol" data-datafeed="myfeed" data-height="480" />
</template>Svelte
<script>
import { onMount } from 'svelte';
export let symbol;
onMount(() => {
window.vpEmbed?.datafeed('myfeed', myFeed);
window.vpEmbed?.scan();
});
</script>
<div class="vp-chart" data-symbol={symbol} data-datafeed="myfeed" data-height="480"></div>Client-side routing, in any of them
The loader scans on load and whenever vpEmbed.scan() is called. A framework that swaps the page without a document load will add placeholders the initial scan never saw, so call it after the route settles. It is idempotent, a placeholder that already has a frame is skipped, so calling it too often costs nothing and calling it too rarely leaves an empty div.
router.afterEach(() => window.vpEmbed?.scan());WordPress
Use a Custom HTML block, not the visual editor, the visual editor rewrites markup it does not recognise and will eat the placeholder’s attributes.
<iframe src="https://view.verticalprop.com/embed?symbol=BTCUSDT&theme=light"
style="width:100%;height:480px;border:0" loading="lazy"></iframe>On WordPress.com and other managed installs, <script> tags are stripped on most plans regardless of block type. The iframe form is the answer there and needs no plugin. If you are self-hosted and want the loader, enqueue it properly from your theme rather than pasting a script tag into post content:
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script(
'vp-embed',
'https://view.verticalprop.com/embed.js',
[], null, true
);
});Ghost
An HTML card takes the iframe form as-is. For the loader, put the script in Settings → Code injection → Site footer once, and then every post can carry bare placeholders.
Webflow, Framer and friends
Use the Embed element with the iframe form. These builders wrap custom HTML in their own containers, which is fine for an iframe and unreliable for a script-plus-placeholder pair, the loader may run before the container exists.
Set the element’s height in the builder as well as on the iframe. Several of them collapse an auto-height embed to zero, which looks exactly like a chart that failed to load.
Notion, Confluence and read-only surfaces
Paste the embed URL directly and the surface will build its own iframe:
https://view.verticalprop.com/embed?symbol=BTCUSDT&interval=60&theme=lightThese surfaces run no JavaScript of yours, so a host datafeed is not available and the embed will need one, see the datafeed page for what that means and why. A chart on a page you cannot add script to is the one case the embed genuinely cannot serve.
If your site has a Content-Security-Policy
You need two entries, and notably you do not need a worker-src or blob: allowance, that is the whole reason this is an iframe rather than a script bundle. Community indicators run inside our frame, under our policy.
frame-src https://view.verticalprop.com;
script-src https://view.verticalprop.com; # only for the loader formDrop the second line if you use the iframe form. Nothing else of ours touches your policy: no fonts, no styles, no XHR, no images.