GkOverlay
A minimal overlay for modal-style content: full-screen scrim, Teleport (default body), v-model visibility, Escape to dismiss, optional persistent mode (scrim and Escape do not close), body scroll lock while open, and tokenized z-index / scrim color.
This is intentionally smaller than Vuetify’s VOverlay: no activator slot, location/scroll strategies, router back, global stack composable, lazy hydration, or full focus-trap library. Compose with a button and v-model in your app (see demo).
When to use
Use as the lowest-level containment primitive when you need teleported layered content with scrim/escape/focus behavior but custom surface markup.
Live Examples
Basic overlay
Use GkOverlay when you want custom surface markup but standard scrim, Escape, focus restore, and body scroll lock.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkOverlay } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkButton type="button" @click="open = true">Open overlay</GkButton>
<GkOverlay
v-model="open"
aria-labelledby="overlay-basic-title"
aria-describedby="overlay-basic-desc"
>
<div class="panel">
<h2 id="overlay-basic-title">GkOverlay</h2>
<p id="overlay-basic-desc">
Scrim click or Escape closes. Body scroll is locked while open.
</p>
<GkButton type="button" variant="secondary" @click="open = false">
Close
</GkButton>
</div>
</GkOverlay>
</template>
<style scoped>
.panel {
padding: var(--gk-space-5);
border-radius: var(--gk-radius-md);
background: var(--gk-color-surface);
color: var(--gk-color-on-surface);
border: 1px solid var(--gk-color-border);
box-shadow: var(--gk-elevation-4);
min-width: min(100%, 18rem);
}
.panel h2 {
margin: 0 0 var(--gk-space-2);
font-size: var(--gk-font-size-md);
line-height: var(--gk-line-height-tight);
}
.panel p {
margin: 0 0 var(--gk-space-4);
font-size: var(--gk-font-size-sm);
line-height: var(--gk-line-height-normal);
color: var(--gk-color-text-muted);
}
</style>Best practice: Always connect aria-labelledby and aria-describedby to visible content when the overlay behaves like a dialog.
Persistent overlay
Persistent mode prevents scrim and Escape dismiss.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkOverlay } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkButton type="button" variant="secondary" @click="open = true">
Open persistent overlay
</GkButton>
<GkOverlay v-model="open" persistent aria-labelledby="overlay-persistent-title">
<div class="panel">
<h2 id="overlay-persistent-title">Persistent overlay</h2>
<p>Scrim and Escape do not dismiss. Provide a clear close action.</p>
<GkButton type="button" @click="open = false">Close</GkButton>
</div>
</GkOverlay>
</template>
<style scoped>
.panel {
padding: var(--gk-space-5);
border-radius: var(--gk-radius-md);
background: var(--gk-color-surface);
border: 1px solid var(--gk-color-border);
box-shadow: var(--gk-elevation-4);
}
.panel h2 {
margin: 0 0 var(--gk-space-2);
font-size: var(--gk-font-size-md);
}
.panel p {
margin: 0 0 var(--gk-space-4);
color: var(--gk-color-text-muted);
}
</style>Best practice: Persistent overlays must include an explicit close or completion action inside the panel.
Custom panel width
Use contentMaxWidth for wide custom panels while keeping overlay behavior consistent.
<script setup lang="ts">
import { ref } from 'vue'
import { GkButton, GkOverlay } from 'god-kit/vue'
const open = ref(false)
</script>
<template>
<GkButton type="button" variant="secondary" @click="open = true">
Wide custom panel
</GkButton>
<GkOverlay
v-model="open"
content-max-width="min(100%, 48rem)"
aria-labelledby="overlay-wide-title"
>
<section class="panel">
<h2 id="overlay-wide-title">Custom overlay surface</h2>
<p>
GkOverlay gives you the layer, scrim, focus restore, and scroll lock.
The surface markup remains yours.
</p>
<GkButton type="button" @click="open = false">Done</GkButton>
</section>
</GkOverlay>
</template>
<style scoped>
.panel {
display: grid;
gap: var(--gk-space-3);
padding: var(--gk-space-5);
border-radius: var(--gk-radius-lg);
background: var(--gk-color-surface);
border: 1px solid var(--gk-color-border);
box-shadow: var(--gk-elevation-4);
}
.panel h2,
.panel p {
margin: 0;
}
.panel p {
color: var(--gk-color-text-muted);
line-height: var(--gk-line-height-normal);
}
</style>Best practice: Keep layout styling in your panel; let GkOverlay own only the layer, scrim, focus, and scroll-lock mechanics.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | Open state; use v-model |
persistent | boolean | false | When true, scrim click and Escape do not close the overlay |
to | string | HTMLElement | 'body' | Teleport target |
zIndex | number | string | — | Overrides --gk-overlay-z-index when set |
scrollLock | boolean | true | Sets document.body.style.overflow = 'hidden' while open (restored on close) |
showScrim | boolean | true | Renders the full-screen scrim |
role | string | 'dialog' | ARIA role for the panel |
ariaModal | boolean | true | Sets aria-modal="true" when true (typical for dialogs) |
restoreFocus | boolean | true | Focuses the first focusable in the panel on open (or the panel), restores the previous active element on close |
overlayClass | unknown | — | Optional class(es) on the fixed overlay root (.gk-overlay) |
contentMaxWidth | string | — | Sets --gk-overlay-content-max-width on the root (e.g. none, min(100%, 28rem)); default in CSS is min(100%, 32rem) |
transitionName | string | 'gk-overlay' | Vue <Transition> name; use gk-bottom-sheet with GkBottomSheet (built-in styles) |
Additional attributes (for example aria-labelledby, aria-describedby, id) are applied to the panel element (the element with role). defineExpose: contentRef — the panel element.
Slots
| Slot | Description |
|---|---|
default | Panel content; clicks do not reach the scrim (@click.stop on the panel) |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | boolean | Emitted when the overlay closes |
click:outside | MouseEvent | Emitted when the scrim is clicked and the overlay will dismiss (not emitted when persistent is true) |
afterEnter | — | Overlay <Transition> finished entering |
afterLeave | — | Overlay <Transition> finished leaving |
Tokens
| Token | Purpose |
|---|---|
--gk-overlay-scrim | Scrim background (defaults via --gk-color-overlay) |
--gk-overlay-z-index | Default stacking order (2000) |
--gk-overlay-content-max-width | Panel max-width (default min(100%, 32rem); GkDialog may override) |
Accessibility notes
- Provide
aria-labelledbyandaria-describedbywhen overlay content represents a dialog-like interaction. - Keep at least one focusable element in overlay content so focus management remains predictable.
- Persistent overlays should always provide an explicit close action inside the content.
Related components
Out of scope (v1)
Activator slot, scroll/location strategies, router.back integration, global overlay stack composable, lazy hydration, and parity with Vuetify’s click-outside beyond scrim dismissal.
