GrLoading

Package: @feugene/granularitycoreGroup: feedback

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 shortdelay does not show the spinner at all if the answer arrived quickly;
  • the whole screen has to be blockedfullscreen for the duration of an operation that must not be interrupted;
  • the overlay is needed imperatively — the v-loading directive instead of a component in the markup.

When to take something else

NeedTake
There is no content at all yetGrSkeleton
The share of what is done is knownGrProgressBar / GrProgressCircle
The wait is inside a buttonGrButton with loading
Uploading a fileGrFileUpload

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…

Code
<GrLoading />

Install

npm i @feugene/granularity

Import

import { GrLoading } from '@feugene/granularity/components/GrLoading'

API

Props

PropTypedefaultDescription
textstring | undefinedundefinedThe caption under the spinner. An empty string removes it entirely. By default — from the locale.
zIndexVarstring | undefinedundefinedThe name of the CSS variable of the layer — an escape hatch past `--gr-z-loading`.
spinnerComponent | undefinedundefinedA spinner component of your own instead of the default icon.
spinnerClassstring | undefinedundefinedExtra classes for the wrapper of the spinner.
spinnerSizenumber | "xs" | "sm" | "md" | "lg" | undefined28The size of the spinner: the scale of the package or an arbitrary one in pixels.
spinnerToneGrIconTone | undefined"neutral"The tone of the spinner from the palette.
animatedboolean | undefinedtrueThe rotation of the spinner. On by default.
backgroundstring | undefinedundefinedA `background-color` of your own. If it is set, the default `--gr-overlay-bg` scrim is removed.
fullscreenboolean | undefinedfalseCover the whole screen (`position: fixed`) instead of the nearest positioned ancestor.
delaynumber | undefined0The delay before showing, in milliseconds: a short load does not flash an overlay.
customClassstring | undefinedundefinedExtra classes for the root of the overlay.

Slots

SlotTypeDescription
defaultanyThe content of the panel as a whole instead of the spinner with a caption.

Events

EventTypeDescription
show[]

Examples 5

Inline section overlay

Invoice list
Use `GrLoading` as an overlay above an existing card or section while async data is refreshing.
No `text` prop here: the caption comes from the active locale — switch RU/EN to see it change.

Inline
<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

Quarterly report
The fast request finishes before the delay elapses, so the overlay never appears.

Delay Slot
<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

While the overlay is up, the form below is `inert`: Tab skips it and screen readers ignore it. The container itself reports `aria-busy`.

Directive
<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

Brand migration
Custom background and a static, tinted spinner adapt the overlay to dense dashboards.

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

Fullscreen overlay closes automatically after a short async cycle.

Fullscreen
<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>

Component documentationAll components