Mount Coras in your framework
This guide shows how to mount Coras Embed in your framework and tear it down cleanly. Every framework uses the same SDK contract from @coras-io/embed, so the only thing that changes per framework is which lifecycle hook calls each method.
Before you start
Section titled “Before you start”- Install
@coras-io/embed. - Have your distributor ID and API URL ready for
config.
The contract
Section titled “The contract”Map these four steps onto your framework’s component lifecycle:
- Get a container DOM element.
- Call
mount({ container, page, config, params })when the component attaches. Keep the returnedCorasApp. - Call
app.update({ ... })whenpage,params, orconfigchange. - Call
app.unmount()when the component detaches.
Layout & styling
Section titled “Layout & styling”There are two ways to lay Coras out, and they call for opposite CSS:
- Standalone - Coras is the site. Use
chrome: "managed"so the mount renders the Coras navbar and footer, and let it own the whole viewport. - Embedded - Coras mounts into an app that already has its own navbar,
footer, and routing. Use
chrome: falseand treat the mount as one content region among many.
Standalone
Section titled “Standalone”Coras renders a complete page - navbar, page content, and footer - into the
container. Let it fill the viewport; do not wrap it in a centered, max-width, or
text-aligned container, and remove any starter-template CSS (a centered
#root/#app, demo :root colour variables, App.css) - that is the usual
cause of clipped content and off-brand colours.
A minimal full-bleed reset is all you need (swap #app for your container’s
selector):
html,body { height: 100%; margin: 0; padding: 0;}
body { display: flex; flex-direction: column;}
#app { flex: 1 0 auto; display: flex; flex-direction: column;}That is the whole of it. The mount is a flex column that grows, and so is the page inside it, so the managed footer sits at the bottom of a short page on its own. You never need a rule that names a Coras element: those names are internal and can change without a major version.
If your framework wrapper puts an element of its own between that container and
the mount - a React <div ref={containerRef} />, say - give it
display: contents so the mount stays a direct flex child:
<div ref={containerRef} style={{ display: "contents" }} />Embedded
Section titled “Embedded”To mount Coras inside an existing app, set chrome: false
so it renders the page alone, without the Coras navbar or footer, and keep the
host’s own chrome around it. Mount into a content region - the host route’s
<main>, say - and do not apply the full-bleed reset above: the mount should
size to its region, not take the viewport.
Scope Coras to its own part of the host’s routes with the URL strategy’s
basePath (for example
basePath: "/tickets"), so buildCorasUrl and parseCorasUrl carry that
prefix and the host’s other routes are left untouched. See the
embedded example
for a full host app.
Framework examples
Section titled “Framework examples”Each tab is copy-paste ready and produces the same runtime behavior. The examples mount on attach and tear down on detach; to react to changing page, params, or config, also call app.update({ ... }) from your framework’s reactive hook (step 3 above).
import { mount } from "@coras-io/embed";
const app = mount({ container: document.querySelector("#coras"), page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" }});
// Tear down when you are done:// app.unmount();Register a reusable corasEmbed data component, then use x-data on the container:
<script type="module"> import { mount } from "@coras-io/embed";
document.addEventListener("alpine:init", () => { Alpine.data("corasEmbed", () => ({ app: null, init() { this.app = mount({ container: this.$el, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); }, destroy() { this.app?.unmount(); }, })); });</script>
<div x-data="corasEmbed"></div>import { useEffect, useRef } from "react";import { mount, type CorasApp } from "@coras-io/embed";
export function CorasEmbed() { const containerRef = useRef<HTMLDivElement>(null); const appRef = useRef<CorasApp | null>(null);
useEffect(() => { appRef.current = mount({ container: containerRef.current!, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); return () => appRef.current?.unmount(); }, []);
return <div ref={containerRef} />;}The embed needs the DOM, so it must live in a client component:
"use client";
import { useEffect, useRef } from "react";import { mount, type CorasApp } from "@coras-io/embed";
export function CorasEmbed() { const containerRef = useRef<HTMLDivElement>(null); const appRef = useRef<CorasApp | null>(null);
useEffect(() => { appRef.current = mount({ container: containerRef.current!, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); return () => appRef.current?.unmount(); }, []);
return <div ref={containerRef} />;}Render <CorasEmbed /> from a server component like any other client component.
Mount inside an Astro component using a client-side <script> block. Astro does not hydrate plain components, so call mount() directly when the script runs.
<div id="coras"></div>
<script> import { mount } from "@coras-io/embed";
mount({ container: document.querySelector<HTMLDivElement>("#coras")!, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } });</script>Vue 3 with <script setup>:
<script setup lang="ts">import { onMounted, onUnmounted, ref } from "vue";import { mount, type CorasApp } from "@coras-io/embed";
const container = ref<HTMLDivElement>();let app: CorasApp | undefined;
onMounted(() => { app = mount({ container: container.value!, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } });});
onUnmounted(() => app?.unmount());</script>
<template> <div ref="container" /></template>Svelte 5 with runes:
<script lang="ts"> import { onMount } from "svelte"; import { mount, type CorasApp } from "@coras-io/embed";
let container: HTMLDivElement; let app: CorasApp | undefined;
onMount(() => { app = mount({ container: container, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); return () => app?.unmount(); });</script>
<div bind:this={container}></div>import { onMount, onCleanup } from "solid-js";import { mount, type CorasApp } from "@coras-io/embed";
export function CorasEmbed() { let container!: HTMLDivElement; let app: CorasApp | undefined;
onMount(() => { app = mount({ container: container, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); onCleanup(() => app?.unmount()); });
return <div ref={container} />;}Standalone component, Angular 17+:
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from "@angular/core";import { mount, type CorasApp } from "@coras-io/embed";
@Component({ selector: "coras-embed", standalone: true, template: `<div #container></div>`,})export class CorasEmbedComponent implements OnInit, OnDestroy { @ViewChild("container", { static: true }) container!: ElementRef<HTMLDivElement>;
private app?: CorasApp;
ngOnInit(): void { this.app = mount({ container: this.container.nativeElement, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); }
ngOnDestroy(): void { this.app?.unmount(); }}A Lit element that hosts the SDK. Render in light DOM (createRenderRoot returns this) so the embed sees the host page’s tokens.
import { LitElement, html } from "lit";import { customElement, query } from "lit/decorators.js";import { mount, type CorasApp } from "@coras-io/embed";
@customElement("my-coras-embed")export class MyCorasEmbed extends LitElement { @query("div") private container!: HTMLDivElement; private app?: CorasApp;
protected createRenderRoot() { return this; }
protected firstUpdated() { this.app = mount({ container: this.container, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); }
override disconnectedCallback() { super.disconnectedCallback(); this.app?.unmount(); }
render() { return html`<div></div>`; }}Glimmer component with render modifiers (@ember/render-modifiers):
import Component from "@glimmer/component";import { action } from "@ember/object";import { mount, type CorasApp } from "@coras-io/embed";
export default class CorasEmbedComponent extends Component { private app?: CorasApp;
@action setup(element: HTMLDivElement) { this.app = mount({ container: element, page: "landing", config: { apiUrl: "https://api.coras.io", distributorId: "your-distributor-id", assetsUrl: "https://assets.sandbox.coras.io/shared", locale: "en-IE", currency: "EUR" } }); }
@action teardown() { this.app?.unmount(); }}
{{! app/components/coras-embed.hbs }}<div {{did-insert this.setup}} {{will-destroy this.teardown}}></div>React to prop changes
Section titled “React to prop changes”The tabs above mount on attach and tear down on detach. When page, params, or config change while the component stays mounted, call app.update({ ... }) from your framework’s reactive hook instead of remounting. It updates in place unless page changes:
// e.g. React useEffect([id]), Vue watch, Svelte $effect, Angular ngOnChangesappRef.current?.update({ page: "details", params: { id } });Server-side rendering
Section titled “Server-side rendering”Page elements need the DOM, so render the embed in a client-only boundary:
| Framework | Client-only boundary |
|---|---|
| Next.js | "use client" directive |
| Nuxt | <ClientOnly> |
| SvelteKit | onMount |
| Astro | client:only |
Troubleshoot
Section titled “Troubleshoot”- Nothing renders and an error is thrown.
mount()validatesconfigandparamsbefore it renders anything. Invalid input throwsCorasValidationError. Check the error message for the failing key. - The embed mounts twice in React development. With React StrictMode, double-mount effects run
mount()twice.unmount()is idempotent and safe, and the second mount replaces the first. - Two embeds on one page interfere. They do not. The SDK scopes theme, chrome, and events to each mount root, so multiple embeds coexist without leakage.
- Icons are missing, the brand looks unstyled, or the page is clipped. Pass
strict: true(or wire alogger) and check the console:mount()warns whenassetsUrlis unreachable,config.themehas no colours, or a managed-chrome page is constrained by an ancestor. Each points at the fix.