---
title: "Recipe: Frame app skeleton"
description: "Minimum viable Bitrix24 iframe app: init handshake, set title, persist app state, open a slider, close cleanly."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/examples/frame-app-skeleton"
last_updated: "2026-06-02"
---
# Recipe: Frame app skeleton

> Minimum viable Bitrix24 iframe app: init handshake, set title, persist app state, open a slider, close cleanly.

> [!WARNING]
> We are still updating this page. Some data may be missing here — we will complete it shortly.

## Goal

A single Vue 3 component that demonstrates the four things every Bitrix24 iframe app needs: initialize the postMessage handshake, set the parent window title, persist a piece of state across reloads via app options, and open another portal page in a slider.

## Stack & Prerequisites

- Vue 3 + Vite (or Nuxt 3 with `@bitrix24/b24jssdk-nuxt`)
- `@bitrix24/b24jssdk@^1.0.0`
- The page must be served from the URL registered as the placement handler in your Bitrix24 app card. Outside the iframe, `initializeB24Frame()` rejects.

```bash
pnpm add @bitrix24/b24jssdk
```

## Full Example

`src/components/AppShell.vue`:

```vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import {
  initializeB24Frame,
  type B24Frame,
  LoggerFactory
} from '@bitrix24/b24jssdk'

const logger = LoggerFactory.createForBrowser('AppShell', import.meta.env.DEV)
const ready = ref(false)
const userTheme = ref<'light' | 'dark'>('light')
const portalDomain = ref('')
let $b24: B24Frame | null = null

onMounted(async () => {
  try {
    $b24 = await initializeB24Frame()
  }
  catch (error) {
    logger.error('initializeB24Frame failed', error)
    return
  }

  $b24.parent.setTitle('My Bitrix24 App')
  // Auto-resize the iframe to fit its content; call again when content height
  // changes (e.g. after a network response renders).
  await $b24.parent.resizeWindowAuto()

  // Read persisted preferences. Returns `null` if nothing was set yet.
  const stored = $b24.options.appGet('theme') as 'light' | 'dark' | null
  if (stored) userTheme.value = stored

  const appInfo = await $b24.actions.v2.call.make({ method: 'app.info' })
  portalDomain.value = appInfo.getData().result.DOMAIN ?? ''

  ready.value = true
})

onBeforeUnmount(() => {
  $b24 = null
})

async function setTheme(value: 'light' | 'dark') {
  userTheme.value = value
  // App-scoped option: persisted on the portal under the app, not the user.
  await $b24?.options.appSet('theme', value)
}

async function openCrmDeals() {
  if (!$b24) return
  const url = new URL('/crm/deal/list/', `https://${portalDomain.value}`)
  await $b24.slider.openPath(url, 1200)
}

async function close() {
  await $b24?.parent.closeApplication()
}
</script>

<template>
  <section v-if="ready" class="app-shell" :data-theme="userTheme">
    <header>
      <h1>Hello from {{ portalDomain }}</h1>
    </header>

    <main>
      <button @click="setTheme(userTheme === 'light' ? 'dark' : 'light')">
        Switch theme (current: {{ userTheme }})
      </button>
      <button @click="openCrmDeals">
        Open deals in slider
      </button>
    </main>

    <footer>
      <button @click="close">Close app</button>
    </footer>
  </section>
  <p v-else>Loading…</p>
</template>
```

`src/main.ts`:

// @check-ignore: vue .vue module import — .vue files are not in tsconfig

```ts
import { createApp } from 'vue'
import AppShell from './components/AppShell.vue'

createApp(AppShell).mount('#app')
```

## Run It

```bash
# 1. Local dev server (Vite)
pnpm dev
# → http://localhost:5173/ — running this URL directly will reject:
#   initializeB24Frame() needs a parent Bitrix24 window.

# 2. Expose the dev server (ngrok / cloudflared / your tunnel of choice)
ngrok http 5173
# → grab the https URL.

# 3. In Bitrix24: Applications → Developer resources → Local app →
#    point the placement handler URL at the tunnel URL and Save.

# 4. Open the placement inside Bitrix24.
```

Once the iframe loads, you'll see: the parent window title becomes "My Bitrix24 App", the iframe resizes to its content, and the body shows `Hello from <your-portal>.bitrix24.com` plus the two buttons. Toggling the theme persists across reloads (the value lives on the portal under the app, not in `localStorage`). Clicking "Open deals in slider" opens `/crm/deal/list/` of the same portal in a Bitrix24-styled slider.

## How It Works

- `initializeB24Frame()` is the only correct way to construct `B24Frame`. It deduplicates concurrent calls, parses portal handshake parameters from `window.name`, and waits for the parent window to acknowledge the auth payload.
- `parent.setTitle()` and `parent.resizeWindowAuto()` reach across the iframe boundary via `postMessage`. Both are idempotent — call them whenever your layout changes.
- `options.appGet()` is synchronous because the values are loaded once at handshake time. `options.appSet()` is asynchronous because it round-trips to the server.
- `slider.openPath(url, width)` accepts a `URL` instance (not a string) and returns a `Promise<StatusClose>` that resolves when the user closes the slider. The portal CSP only allows opening URLs on the same portal domain.

## Limitations

- The component must mount **inside** the Bitrix24 iframe; running it standalone (e.g. in Storybook) fails immediately because the parent window is missing. Guard for `window.parent === window` if you need a graceful fallback.
- `$b24.options.appSet()` writes one option at a time. Batching multiple writes inside a single render is fine (the SDK serializes them), but each call is a network round-trip.
- `parent.closeApplication()` only closes the app slider when the placement supports closing. Embedded placements (sidebar widgets) ignore the request.
- App-scoped options are visible to every user of the app on the portal. Use `userGet` / `userSet` for per-user preferences.

## See also

- [`initializeB24Frame()`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/frame-initialize-b24-frame.md)
- [`B24Frame.parent`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/frame-parent.md) — title, resize, close, IM helpers.
- [`B24Frame.slider`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/frame-slider.md)
- [`B24Frame.options`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/frame-options.md) — app and user option storage.

## Sitemap

See the full [sitemap](/b24jssdk/sitemap.md) for all pages.
