---
title: Visual Editor
description: Discover Storyblok's documentation with comprehensive developer guides, user manuals, API references, and examples to help you get the most out of the headless CMS platform.
url: https://storyblok.com/docs/concepts/visual-editor
---

# Visual Editor

Most CMSs require users to choose between integration and customization. Seamless integration with the CMS often means less freedom to customize your website, or vice versa.

Storyblok offers the best of both worlds: a WYSIWYG editing experience embedded directly in the Visual Editor, with complete freedom to customize both the website's backend and frontend.

> [!TIP]
> Learn how to create and manage content in the [Visual Editor manual](/docs/manuals/visual-editor).

The Visual Editor supports various interactions:

-   Click on a block in the editor to scroll to the corresponding element in the preview area, or click on an element in the preview to open the block in the editor
-   Orient yourself with outlines that appear around block elements in the preview area
-   Click elements in the preview area and use context menus to navigate between blocks
-   Make changes in the editor and view the updated content in real-time

These features deliver an intuitive experience for everyone on your team—from sales reps to HR managers, developers, and marketers.

Provide this seamless editing using the following model:

## Implementation

1.  **Fetch draft content**
    
    To render a preview in the editor, fetch the draft version of the content. Learn more about [creating a preview deployment](https://www.storyblok.com/tp/create-preview-production-environments-and-deploy).
    
    ```javascript
    import { storyblokInit, apiPlugin } from '@storyblok/js';
    
    const { storyblokApi } = storyblokInit({
      accessToken: 'YOUR_ACCESS_TOKEN',
      use: [apiPlugin],
    });
    
    // Fetch content
    const { data } = await storyblokApi.get("cdn/stories", {
      version: "draft",
    });
    ```
    
    The draft version of each story’s API response attaches a private `_editable` property to every block. This property enables live editing and instructs the Visual Editor how to handle the elements on the frontend:
    
    ```json
    "body": [
      {
        "_uid": "2db95a85-e979-da7f-895b-c3a41fbc5f8d",
        "component": "page",
        "_editable": "\u003C!--#storyblok ... --\u003E"
    // Shortened for brevitiy
      }
    ]
    ```
    
    The `_editable` property contains a string of encoded JSON. When extracted and parsed, the JSON has four properties:
    
    -   `name`: the block's name
    -   `space`: the numeric space ID
    -   `uid`: the block's unique ID
    -   `id`: the numeric story ID
    
    > [!TIP]
    > The diffeence between `uuid`, `id`, and `_uid`:
    > 
    > The `uuid` is the unique ID of a story, which persists across your Storyblok space. This is the default story identifier.
    > 
    > The `id` is the legacy story ID. Always use the `uuid` in projects.
    > 
    > The `_uid` is the unique ID of a block. It's required for certain functionalities, such as opening components in the editor when clicking them in the preview area. A block's `_uid` is only unique within the scope of an individual story.
    
    Add these properties to your HTML so the Visual Editor can access them.
    
2.  **Add HTML attributes**
    
    Each block has two native data attributes to extract the values of the `_editable` property:
    
    -   `data-blok-c` contains the stringified JSON
    -   `data-blok-uid` contains the ID and UID in the pattern `id-uid`
    
    Storyblok's SDKs include utilities to format the data attributes:
    
    ```javascript
    const editableOptions = storyblokEditable(blok);
    
    const element = `
      <div
        class="storyblok__outline"
        data-blok-c="${editableOptions["data-blok-c"]}"
        data-blok-uid="${editableOptions["data-blok-uid"]}"
      <!-- Content <!-- Content -->
     </div>`;
    ```
    
    Finally, connect the frontend to the Visual Editor using the `StoryblokBridge`.
    
3.  **Enable the preview bridge**
    
    The [@storyblok/preview-bridge](https://www.storyblok.com/docs/libraries/js/preview-bridge) handles most of the heavy lifting required for live editing.
    
    Storyblok hosts the script on a dedicated CDN:
    
    ```bash
    https://app.storyblok.com/f/storyblok-v2-latest.js
    ```
    
    Either load it manually via the URL above, or use one of Storyblok’s frontend SDKs, such as [@storyblok/js](https://www.storyblok.com/docs/libraries/js/js-sdk):
    
    ```javascript
     import { storyblokInit, apiPlugin } from '@storyblok/js';
     import { storyblokInit, apiPlugin, useStoryblokBridge } from '@storyblok/js';
    
    const { storyblokApi } = storyblokInit({
      accessToken: 'YOUR_ACCESS_TOKEN',
      use: [apiPlugin],
    });
    
    // Fetch content
    const { data } = await storyblokApi.get("cdn/stories", {
      version: "draft",
    });
    
     // Activate the StoryblokBridge
     useStoryblokBridge(data.story.id);
    ```
    
    Once activated, the bridge reloads the page each time the editor records a `save` or `publish` event.
    
    Customize and adjust the bridge's behavior with a manual implementation of the `StoryblokBridge` class. Learn more about the supported events and method in [the package reference](https://www.storyblok.com/docs/libraries/js/preview-bridge).
    
    > [!NOTE]
    > If you write custom logic for bridge events, handle slug changes to avoid a `404` error when the page refreshes.
    
    The bridge is responsible for displaying context menus when the user clicks a block.
    
    -   When `save` and `publish` events are configured, the menu display navigation actions.
    -   When the `input` event is configured, the menu also displays editing actions.
    
    The website is now ready to communicate with Storyblok's Visual Editor.
    

## Configure URLs

The Visual Editor loads your webpage in an iframe. To view it, open **Settings** → **Visual Editor** and define a **Preview URL**.

> [!NOTE]
> If the path to your webpage is different from the associated story’s slug, open **Config** in the Visual Editor, and define a **Real path**. This doesn't affect the story’s endpoint or slug.

The preview environment can be a dedicated deployment that builds from the same codebase as the production deployment, and loads preview functionality based on variables.

To learn more, follow the [How to Create Preview & Production Environments and Deploy Your Website](https://www.storyblok.com/tp/create-preview-production-environments-and-deploy) tutorial.

### Preview slug

When the Visual Editor loads the page, it builds a URL from the base domain (`example.com`) and the story’s full slug (`folder/example-uid`).

It also appends several parameters to the URL:

-   `_storyblok`: the numeric story ID
-   `_storyblok_tk[space_id]`: the numeric space ID
-   `_storyblok_tk[timestamp]`: a UNIX timestamp
-   `_storyblok_tk[token]`: a validation token that combines `_storyblok_tk[space_id]`, `_storyblok_tk[timestamp]`, and the preview access token
-   `_storyblok_release`: the numeric release ID (requires the [Releases App](https://www.storyblok.com/apps/releases_only))
-   `_storyblok_lang`: the numeric language ID
-   `_storyblok_c` : the block's name (content type)

The result would look like this:

```plaintext
https://example.com/folder/example-uid?_storyblok=580906535&_storyblok_c=page&_storyblok_version=&_storyblok_lang=default&_storyblok_release=0&_storyblok_rl=1732540047643&_storyblok_tk[space_id]=313862&_storyblok_tk[timestamp]=1732540047&_storyblok_tk[token]=9d25c03de1478da57e37a166a7c053ce0aff9234
```

## Security

Depending on your local or server configurtion, the webpage embedded in the `iframe` might be blocked from the parent page (the Storyblok editor) or from the source page (your website).

If you encounter any problems when loading a webpage in the Visual Editor, add an SSL certificate or adjust the website's [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP).

### SSL certificate

Storyblok's security policy requires that you serve the preview over HTTPS (whether deployed or on localhost). To add an SSL certificate, use the tool that matches your web development framework:

-   For Vite-based frameworks, use [vite-plugin-mkcert](https://github.com/liuweiGL/vite-plugin-mkcert).
-   For Next.js, enable the [built-in HTTPS support](https://vercel.com/guides/access-nextjs-localhost-https-certificate-self-signed).
-   For other frameworks, use `mkcert` ([macOS,](https://www.storyblok.com/faq/setup-dev-server-https-proxy) [Windows](https://www.storyblok.com/faq/setup-dev-server-https-windows))

### Content Security Policy

The website's CSP might block clients from loading pages inside an `<iframe>`. To allow embedding, add Storyblok to the `frame-ancestor` [directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors):

```plaintext
Content-Security-Policy: frame-ancestors https://app.storyblok.com
```

## White labeling

Organization administrators can add a custom logo and custom domain in their organization settings, depending on their subscription plan. See the [pricing page](https://www.storyblok.com/pricing) for more details.

To override the Storyblok branding manually, a webpage can embed the Storyblok application. Storyblok serves a script that can run the entire web app at `https://app.storyblok.com/f/app-latest.js`. With this script, a developer can embed the app on any webpage and style the app interface as needed.

To embed the app on an empty page, create an index.html file:

index.html

```html
<!DOCTYPE html>
<html>
 <head>
   <title>Example Title</title>
 </head>
 <body>
   <div id="app"></div>
   <script src="https://app.storyblok.com/f/app-latest.js" type="text/javascript"></script>
 </body>
</html>
```

 Example Title  

 
