GrLoading
Indicates that a section or action is currently loading.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the content is already there and is being updated — a table is being re-requested, a form is being submitted: the old data is visible but unavailable;
- the wait is short —
delaydoes not show the spinner at all if the answer arrived quickly; - the whole screen has to be blocked —
fullscreenfor the duration of an operation that must not be interrupted; - the overlay is needed imperatively — the
v-loadingdirective instead of a component in the markup.
When to take something else
| Need | Take |
|---|---|
| There is no content at all yet | GrSkeleton |
| The share of what is done is known | GrProgressBar / GrProgressCircle |
| The wait is inside a button | GrButton with loading |
| Uploading a file | GrFileUpload |
Two modes
By default the overlay lies on the nearest positioned ancestor — that is the loading of a
piece of the page. fullscreen covers the whole screen: that is loading at the level of the
application.
The directive chooses the mode itself: target = document.body → fullscreen, otherwise
inline. To a container with position: static it temporarily gives relative, and to a
container with rounding overflow: hidden, so that the blurred backdrop does not come out of
the rounded shape.
The delay
delay (ms) postpones the showing. A request that answered faster than the delay does not
show the overlay at all — flickering is more irritating than the absence of an indicator.
<GrLoading v-if="pending" :delay="200" />
The countdown starts from the mounting, that is, from the moment the loading started. While it is running, the content is not blocked: otherwise a fast request would silently freeze the form.
Accessibility
The overlay is a role="status" with aria-live="polite": the caption is read by a screen
reader at the moment it appears, without interrupting the user.
Covering it visually is not enough: without blocking, the tab goes into a form that is no
longer visible, and a screen reader reads it as an ordinary one. At the moment of showing,
the directive therefore declares the container aria-busy="true" and gives inert to its
other children — the subtree as a whole drops out of the tab order, out of pointer events and
out of the accessibility tree. On closing, inert is removed only from what it set itself,
and the focus returns to where it was if the user has not moved it themselves.
The declarative <GrLoading> does not dispose of its neighbours — it does not know them. If
blocking is needed on the same container, take the directive.
There is deliberately no focus trap inside the overlay: there is nothing to trap in it, and
the inert on the neighbours covers the task as a whole.
The layer
The full-screen mode sits on the --gr-z-loading token (1150) — above the modals, because
it blocks the whole application, and below the toasts, so as not to hide a notification about
a background error. The inline mode has nothing to do with the scale: z-10 is the order
inside its own container.
zIndexVar replaces the layer variable with one of your own — an escape hatch of the same
kind as in useFloating. The component does not accept a raw number: see docs/z-index.md.
The scrim
The dimming under the panel is the --gr-overlay-bg role of the theme, the same one
GrModal and GrDrawer use: a loading overlay has to look like the other modal layers and
to change density together with the theme.
The background prop sets a background-color of your own and cancels the scrim entirely —
it is needed when the overlay lies on an already dimmed surface.
The spinner and the content
spinnerSize and spinnerTone go through GrIcon — the scale of the icons and the text
tokens rather than pixels in the markup. spinner replaces the icon itself, and animated
switches the rotation off.
The slot replaces the content of the panel as a whole — progress with percentages, a button to cancel a long operation:
<GrLoading>
<GrProgressBar :value="percent" class="w-48" />
<GrButton size="xs" variant="outline" @click="abort">Cancel</GrButton>
</GrLoading>
The caption comes from the locale by default (gr.loading.defaultText); text sets one of
your own, and an empty string removes it entirely.
The directive
const controller = createLoading({ target: '#report', text: 'Computing the report', delay: 200 })
controller.setText('Almost done')
controller.close()
v-loading accepts either a boolean or an object of options (the same as the component’s,
plus target and fullscreen). Changing target or the mode recreates the overlay,
everything else is updated in place.
Playground 8
Loading…
<GrLoading />Install
npm i @feugene/granularityImport
import { GrLoading } from '@feugene/granularity/components/GrLoading'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
text | string | undefined | undefined | The caption under the spinner. An empty string removes it entirely. By default — from the locale. |
zIndexVar | string | undefined | undefined | The name of the CSS variable of the layer — an escape hatch past `--gr-z-loading`. |
spinner | Component | undefined | undefined | A spinner component of your own instead of the default icon. |
spinnerClass | string | undefined | undefined | Extra classes for the wrapper of the spinner. |
spinnerSize | number | "xs" | "sm" | "md" | "lg" | undefined | 28 | The size of the spinner: the scale of the package or an arbitrary one in pixels. |
spinnerTone | GrIconTone | undefined | "neutral" | The tone of the spinner from the palette. |
animated | boolean | undefined | true | The rotation of the spinner. On by default. |
background | string | undefined | undefined | A `background-color` of your own. If it is set, the default `--gr-overlay-bg` scrim is removed. |
fullscreen | boolean | undefined | false | Cover the whole screen (`position: fixed`) instead of the nearest positioned ancestor. |
delay | number | undefined | 0 | The delay before showing, in milliseconds: a short load does not flash an overlay. |
customClass | string | undefined | undefined | Extra classes for the root of the overlay. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The content of the panel as a whole instead of the spinner with a caption. |
Events
| Event | Type | Description |
|---|---|---|
show | [] | — |
Examples 5
Inline section overlay
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrLoading } from '@feugene/granularity'
const loading = ref(false)
</script>
<template>
<div class="grid gap-3">
<GrButton class="justify-self-start" @click="loading = !loading">
{{ loading ? 'Hide' : 'Show' }} inline loading
</GrButton>
<div class="relative min-h-[180px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
<div class="font-medium text-[var(--gr-fg)]">Invoice list</div>
<div>Use `GrLoading` as an overlay above an existing card or section while async data is refreshing.</div>
<div>No `text` prop here: the caption comes from the active locale — switch RU/EN to see it change.</div>
</div>
<GrLoading v-if="loading" />
</div>
</div>
</template>Delay and custom panel
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrLoading, GrProgressBar } from '@feugene/granularity'
const fastLoading = ref(false)
const exportLoading = ref(false)
const percent = ref(0)
let exportTimer: number | undefined
// Быстрый ответ: задержка 300 мс не даёт оверлею мигнуть.
function runFast() {
fastLoading.value = true
window.setTimeout(() => {
fastLoading.value = false
}, 200)
}
function runExport() {
exportLoading.value = true
percent.value = 0
exportTimer = window.setInterval(() => {
percent.value = Math.min(100, percent.value + 8)
if (percent.value === 100)
abortExport()
}, 220)
}
function abortExport() {
window.clearInterval(exportTimer)
exportLoading.value = false
}
</script>
<template>
<div class="grid gap-3">
<div class="flex flex-wrap gap-3">
<GrButton variant="outline" @click="runFast">
Fast request (200 ms)
</GrButton>
<GrButton @click="runExport">
Export report
</GrButton>
</div>
<div class="relative min-h-[200px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
<div class="font-medium text-[var(--gr-fg)]">Quarterly report</div>
<div>The fast request finishes before the delay elapses, so the overlay never appears.</div>
</div>
<GrLoading v-if="fastLoading" :delay="300" text="Refreshing..." />
<GrLoading v-if="exportLoading" custom-class="rounded-xl">
<div class="text-sm font-medium text-[var(--gr-fg)]">Building the export</div>
<GrProgressBar :value="percent" class="w-52" />
<GrButton size="xs" variant="outline" @click="abortExport">
Cancel
</GrButton>
</GrLoading>
</div>
</div>
</template>Directive with content blocking
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrInput, vLoading } from '@feugene/granularity'
const loading = ref(false)
const name = ref('Alan Turing')
function save() {
loading.value = true
window.setTimeout(() => {
loading.value = false
}, 2000)
}
</script>
<template>
<div class="grid gap-3">
<GrButton class="justify-self-start" :disabled="loading" @click="save">
Save profile
</GrButton>
<div class="text-xs text-[var(--gr-muted-fg)]">
While the overlay is up, the form below is `inert`: Tab skips it and screen readers ignore it.
The container itself reports `aria-busy`.
</div>
<div
v-loading="{ loading, text: 'Saving profile...', delay: 150 }"
class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4"
>
<div class="grid gap-3">
<GrInput v-model="name" aria-label="Full name" />
<GrButton variant="outline" class="justify-self-start">
Reset
</GrButton>
</div>
</div>
</div>
</template>Custom appearance
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrLoading } from '@feugene/granularity'
const loading = ref(false)
</script>
<template>
<div class="grid gap-3">
<GrButton variant="outline" class="justify-self-start" @click="loading = !loading">
Toggle custom overlay
</GrButton>
<div class="relative min-h-[180px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
<div class="font-medium text-[var(--gr-fg)]">Brand migration</div>
<div>Custom background and a static, tinted spinner adapt the overlay to dense dashboards.</div>
</div>
<GrLoading
v-if="loading"
text="Preparing migration plan..."
background="color-mix(in srgb, var(--gr-fg) 78%, transparent)"
custom-class="rounded-xl"
spinner-tone="primary"
:spinner-size="36"
:animated="false"
/>
</div>
</div>
</template>Fullscreen async cycle
<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrLoading } from '@feugene/granularity'
const loading = ref(false)
function runFullscreenSync() {
loading.value = true
window.setTimeout(() => {
loading.value = false
}, 1400)
}
</script>
<template>
<div class="grid gap-3">
<GrButton class="justify-self-start" @click="runFullscreenSync">
Simulate global sync
</GrButton>
<div class="text-xs text-[var(--gr-muted-fg)]">
Fullscreen overlay closes automatically after a short async cycle.
</div>
<GrLoading v-if="loading" fullscreen text="Syncing workspace data..." style="--gr-muted-fg: white;" />
</div>
</template>