Skip to content

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.

Loader form
<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 form, no script of ours in your page
<iframe src="https://view.verticalprop.com/embed?symbol=BTCUSDT&datafeed=host"
        style="width:100%;height:480px;border:0" loading="lazy"></iframe>
Reach for the iframe form whenever a platform strips <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).

But “no script of ours” and “no JavaScript” are different things, and the difference is the whole point. If you are happy to write JavaScript but will not load a remote <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.
FormJavaScript on your pageWhose codeCan supply data / drive
embed.jsyesours, fetched from our originyes
Your own bridgeyesyoursyes
Bare iframenone–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.

Save the declaration file once and 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.
Chart.tsx
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.

Do not remount on a symbol change. Changing 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.

app/layout.tsx
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

VerticalChart.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

VerticalChart.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:

functions.php
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=light

These 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 form

Drop the second line if you use the iframe form. Nothing else of ours touches your policy: no fonts, no styles, no XHR, no images.