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.
pnpm add @bitrix24/b24jssdk
Full Example
src/components/AppShell.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
import { createApp } from 'vue'
import AppShell from './components/AppShell.vue'
createApp(AppShell).mount('#app')
Run It
# 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 constructB24Frame. It deduplicates concurrent calls, parses portal handshake parameters fromwindow.name, and waits for the parent window to acknowledge the auth payload.parent.setTitle()andparent.resizeWindowAuto()reach across the iframe boundary viapostMessage. 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 aURLinstance (not a string) and returns aPromise<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 === windowif 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/userSetfor per-user preferences.
See also
initializeB24Frame()B24Frame.parent— title, resize, close, IM helpers.B24Frame.sliderB24Frame.options— app and user option storage.
OAuth install
Express server that handles ONAPPINSTALL / ONAPPUPDATE / ONAPPUNINSTALL events for a Bitrix24 marketplace app, persists tokens per portal, and builds a B24OAuth client on demand with a refresh callback wired to storage.
Mass messaging
Filter contacts in CRM, personalise a template, send IM notifications to assigned managers.