Skip to main content

Shadow Views — Vue

This page covers the Vue-specific implementation. For shared concepts — @View, @InjectView, lifecycle control, and anchor selection — see Shadow Views.

Prerequisites​

Vue Shadow Views require:

{
"ui": {
"framework": "vue"
}
}

in your hexa-cli.config.json. Also ensure vue@^3.5 and @vitejs/plugin-vue@^5 are installed.

Complete example​

A Shadow View has two files: the view class (.ts) and the Vue SFC (.vue). They always live side by side.

The view class​

The view class extends HexaView, holds domain state, and exposes lifecycle helpers. The component field points to a .vue SFC.

// grayscale-toggle-view.ts
import { HexaView, View } from '@hexajs-dev/core';
import GrayscaleToggleComponent from './grayscale-toggle.component.vue';
import styles from './grayscale-toggle.css?inline';

@View({
id: 'hexa-grayscale-toggle',
component: GrayscaleToggleComponent,
styles,
anchorSelector: 'body',
})
export class GrayscaleToggleView extends HexaView {
private enabled = false;

isEnabled(): boolean {
return this.enabled;
}

setEnabled(enabled: boolean): void {
this.enabled = enabled;
document.documentElement.classList.toggle('hexa-grayscale-enabled', enabled);
}

toggle = (): boolean => {
const nextEnabled = !this.enabled;
this.setEnabled(nextEnabled);
return nextEnabled;
};

reset(): void {
this.enabled = false;
document.documentElement.classList.remove('hexa-grayscale-enabled');
}
}

The Vue SFC​

The component is a Vue 3 SFC using <script setup>. HexaJS passes the view instance as the controller prop.

<!-- grayscale-toggle.component.vue -->
<template>
<button
type="button"
class="hexa-grayscale-toggle"
@click="handleClick"
:aria-label="enabled ? 'Disable grayscale' : 'Enable grayscale'"
:title="enabled ? 'Disable grayscale' : 'Enable grayscale'"
>
<span class="hexa-grayscale-toggle__icon">
<svg v-if="enabled" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M3.78 4.84L2.72 5.9l3.11 3.11A11.81 11.81 0 0 0 2.9 12c1.7 3.43 5.15 5.75 9.1 5.75c1.66 0 3.23-.41 4.62-1.14l3.76 3.76l1.06-1.06L3.78 4.84z" />
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M12 6.25c4.09 0 7.36 2.5 8.85 5.75c-1.49 3.25-4.76 5.75-8.85 5.75S4.64 15.25 3.15 12C4.64 8.75 7.91 6.25 12 6.25z" />
</svg>
</span>
<span class="hexa-grayscale-toggle__label">{{ enabled ? 'On' : 'Off' }}</span>
</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { GrayscaleToggleView } from './grayscale-toggle-view';

const props = defineProps<{
controller: GrayscaleToggleView;
}>();

const enabled = ref(props.controller.isEnabled());

function handleClick(): void {
enabled.value = props.controller.toggle();
}
</script>

The stylesheet​

Import styles with the ?inline query so Vite returns the CSS as a string. HexaJS injects it into the Shadow Root.

Use :host { all: initial; } as the first rule to reset all inherited styles at the Shadow Root boundary before your scoped rules apply.

/* grayscale-toggle.css */
:host {
all: initial;
}

.hexa-grayscale-toggle {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 2147483646;
border: 1px solid #d1d5db;
background: #ffffff;
color: #111827;
border-radius: 999px;
box-shadow: 0 10px 26px rgba(2, 6, 23, 0.25);
height: 44px;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 12px;
font-weight: 700;
}

.hexa-grayscale-toggle:hover {
background: #f9fafb;
}

.hexa-grayscale-toggle:focus-visible {
outline: 2px solid #111827;
outline-offset: 2px;
}

Patterns​

Type controller against the view class​

Always use the concrete view class as the prop type, not a partial interface. This gives you full autocomplete on domain methods.

// correct — typed against the class
const props = defineProps<{
controller: GrayscaleToggleView;
}>();

// avoid — loses type safety on domain methods
const props = defineProps<{
controller: { toggle: () => boolean };
}>();

Bridge controller state into Vue reactivity​

The view class owns domain state. To make that state reactive in the template, copy the initial value into a ref and update it via controller method return values.

// read initial value once
const enabled = ref(props.controller.isEnabled());

// update reactive ref through the controller
function handleClick(): void {
enabled.value = props.controller.toggle();
}

Avoid reading props.controller state directly in the template — controller properties are plain class fields, not reactive. Bridge them into ref or computed first.

Always include :host { all: initial; }​

Without this rule, Shadow DOM does not fully isolate inherited styles. The page's font-family, color, line-height, and other inheritable properties bleed through. Add it as the first rule in every view's stylesheet.

How VueShadowRenderer works​

When mount() is called on the view, HexaJS runs this sequence:

  1. resolve document.querySelector(anchorSelector || 'body'),
  2. create a host element named hexa-${id},
  3. attach an open Shadow Root,
  4. inject a <style> tag with your inline CSS,
  5. create a mount <div> inside the Shadow Root,
  6. set mountElement.style.all = 'initial',
  7. call createApp(YourComponent, { controller: viewInstance }).mount(mountElement),
  8. return a teardown that calls app.unmount() and removes the host element.

Suggested file structure​

src/
content/
ui/
grayscale-toggle/
grayscale-toggle-view.ts ← HexaView subclass + @View decorator
grayscale-toggle.component.vue ← Vue SFC
grayscale-toggle.css ← scoped styles (imported ?inline)