GrLoading
Берут, когда контент уже есть и обновляется.
Когда брать
- контент уже есть и обновляется — таблица перезапрашивается, форма отправляется: старое видно, но недоступно;
- ожидание короткое —
delayне показывает спиннер вовсе, если ответ пришёл быстро; - блокировать надо весь экран —
fullscreenна время операции, которую нельзя прерывать; - оверлей нужен императивно — директива
v-loadingвместо компонента в разметке.
Когда взять другое
| Нужно | Берите |
|---|---|
| Контента ещё нет вовсе | GrSkeleton |
| Известна доля выполненного | GrProgressBar / GrProgressCircle |
| Ожидание внутри кнопки | GrButton с loading |
| Загрузка файла | GrFileUpload |
Два режима
По умолчанию оверлей ложится на ближайшего позиционированного предка — это
загрузка куска страницы. fullscreen накрывает весь экран: это загрузка уровня
приложения.
Директива режим выбирает сама: target = document.body → fullscreen, иначе
инлайновый. Контейнеру со position: static она временно ставит relative, а
контейнеру со скруглением — overflow: hidden, чтобы размытая подложка не
вылезала за скруглённую форму.
Задержка
delay (мс) откладывает показ. Запрос, ответивший быстрее задержки, не
показывает оверлей вовсе — мигание раздражает сильнее, чем отсутствие
индикации.
<GrLoading v-if="pending" :delay="200" />
Отсчёт начинается с монтирования, то есть с момента старта загрузки. Пока он идёт, контент не блокируется: иначе быстрый запрос молча замораживал бы форму.
Доступность
Оверлей — role="status" с aria-live="polite": подпись читается диктором в
момент появления, не перебивая пользователя.
Визуального перекрытия недостаточно: без блокировки таб уходит в форму, которую
уже не видно, а диктор читает её как обычную. Директива поэтому в момент показа
объявляет контейнер aria-busy="true", а его остальным детям ставит inert —
поддерево целиком выпадает из таб-порядка, из событий указателя и из дерева
доступности. При закрытии inert снимается только с того, что поставила она
сама, и фокус возвращается туда, где был, если пользователь не увёл его сам.
Декларативный <GrLoading> соседями не распоряжается — он их не знает.
Нужна блокировка на этом же контейнере — берите директиву.
Фокус-ловушки внутри оверлея нет намеренно: ловить в нём нечего, а inert
соседей закрывает задачу целиком.
Слой
Полноэкранный режим сидит на токене --gr-z-loading (1150) — выше модалок,
потому что блокирует приложение целиком, и ниже тостов, чтобы не прятать
уведомление о фоновой ошибке. Инлайновый режим к шкале отношения не имеет:
z-10 — порядок внутри своего контейнера.
zIndexVar подменяет переменную слоя своей — escape-hatch того же вида, что у
useFloating. Сырого числа компонент не принимает: см. docs/z-index.md.
Скрим
Затемнение под панелью — роль темы --gr-overlay-bg, та же, что у GrModal и
GrDrawer: оверлей загрузки обязан выглядеть как остальные модальные слои и
менять плотность вместе с темой.
Проп background задаёт свой background-color и отменяет скрим целиком —
нужен, когда оверлей ложится на уже затемнённую поверхность.
Спиннер и содержимое
spinnerSize и spinnerTone идут через GrIcon — шкала иконок и токены
текста, а не пиксели в разметке. spinner подменяет саму иконку, animated
выключает вращение.
Слот заменяет содержимое панели целиком — прогресс с процентами, кнопку отмены долгой операции:
<GrLoading>
<GrProgressBar :value="percent" class="w-48" />
<GrButton size="xs" variant="outline" @click="abort">Отменить</GrButton>
</GrLoading>
Подпись по умолчанию берётся из локали (gr.loading.defaultText); text
задаёт свою, пустая строка убирает совсем.
Директива
const controller = createLoading({ target: '#report', text: 'Считаем отчёт', delay: 200 })
controller.setText('Почти готово')
controller.close()
v-loading принимает либо булево, либо объект опций (те же, что у компонента,
плюс target и fullscreen). Смена target или режима пересоздаёт оверлей,
всё остальное обновляется на месте.
Playground 8
Загружается…
<GrLoading />Установка
npm i @feugene/granularityИмпорт
import { GrLoading } from '@feugene/granularity/components/GrLoading'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
text | string | undefined | undefined | Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. |
zIndexVar | string | undefined | undefined | Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. |
spinner | Component | undefined | undefined | Свой компонент спиннера вместо иконки по умолчанию. |
spinnerClass | string | undefined | undefined | Дополнительные классы обёртки спиннера. |
spinnerSize | number | "xs" | "sm" | "md" | "lg" | undefined | 28 | Размер спиннера: шкала пакета либо произвольный в пикселях. |
spinnerTone | GrIconTone | undefined | "neutral" | Тон спиннера из палитры. |
animated | boolean | undefined | true | Вращение спиннера. По умолчанию включено. |
background | string | undefined | undefined | Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. |
fullscreen | boolean | undefined | false | Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. |
delay | number | undefined | 0 | Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. |
customClass | string | undefined | undefined | Дополнительные классы корня оверлея. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое панели целиком вместо спиннера с подписью. |
Events
| Event | Type | Описание |
|---|---|---|
show | [] | — |
Примеры 5
Оверлей поверх куска страницы
Базовый сценарий: оверлей поверх карточки, пока обновляются данные. Подпись читается диктором — у корня role="status".
<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 не даёт оверлею мигнуть на быстром ответе, а слот заменяет содержимое панели — прогресс и кнопка отмены.
<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>Директива с блокировкой содержимого
Директива v-loading объявляет контейнер aria-busy и помечает его содержимое inert: под оверлеем не остаётся ни таб-порядка, ни доступного дерева.
<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>Свой вид
Настройка background, spinnerTone и spinnerSize под плотные дашборды; animated выключает вращение.
<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>Полноэкранный цикл ожидания
Полноэкранный режим на токене --gr-z-loading: он выше модалок, потому что блокирует приложение целиком.
<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>