GrAffix

  • ядро
  • Утилиты
  • API · 3 props

Берут, когда действия длинной формы.

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

Обзор

Когда брать

  • действия длинной формы — «Сохранить» не должен уезжать за экран на двадцатом поле; панель прилипает к низу и отпускается, когда форма кончилась;
  • заголовок раздела в длинном списке — читатель всегда видит, где находится, а не отматывает вверх;
  • оглавление или фильтры сбоку — колонка едет с содержимым и останавливается под шапкой приложения; подсветку раздела в этой колонке даёт GrScrollSpy;
  • панель внутри своего скроллера — в раздвижной области, в теле окна, в прокручиваемой карточке: край берётся у ближайшего скроллпорта, а не у окна;
  • вид меняется от того, прилипло ли — подзаголовок уходит, кнопка ужимается до иконки: состояние приезжает в слот и в data-stuck.

Когда взять другое

НужноБерите
Шапка приложения с зонами, кнопкой меню и лендмаркомGrNavbar
Заголовок таблицы, стоящий на месте при прокрутке телаGrTable / GrDataTable
Нижняя навигация по разделам на мобильномGrBottomNav
Подсветить пункт оглавления, до которого дочиталиGrScrollSpy
Панель поверх страницы, а не в её потокеGrDrawer / GrModal

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Панель, которая знает, что прилипла

    Панель прилипает к краю своего скроллера, а не окна. stuck приходит в слот: когда заголовок наверху уехал, панель повторяет номер и сумму, и «Send» не нажимают вслепую. Поверхность и тень приходят вместе с прилипанием — отлипшая, панель остаётся частью страницы.

    INV-0418 DraftNorthwind Traders · due 21 October 2026
    7 lines
    4 × Ergonomic chair€1,156.00
    2 × Standing desk€1,280.00
    12 × Monitor arm€948.00
    4 × Desk lamp€216.00
    2 × Whiteboard€290.00
    1 × Delivery to Hamburg€120.00
    1 × Assembly on site€240.00
    Total€4,250.00
  • Заголовки разделов под шапкой приложения

    Каждый заголовок прилипает под уже закреплённой шапкой и только внутри своего раздела — следующий раздел его выталкивает. Отступ задан один раз для всех токеном --gr-affix-offset на контейнере; наблюдатель читает вычисленный стиль, поэтому 3rem и var(…) работают так же, как проп.

    Billing settings

    Invoices

    Numbering: INV-0001
    Payment terms: 14 days
    Default currency: EUR
    Footer note on every PDF

    Reminders

    First reminder: 3 days after due
    Second reminder: 10 days after due
    Copy to the account owner

    Taxes

    VAT number: DE 812 345 678
    Reverse charge for EU customers
    Rates by country of delivery
  • «Сохранить» под рукой, пока есть что сохранять

    placement="bottom" держит действия у нижнего края, пока форму читают, и отпускает их на место в её конце. disabled выключает прилипание, пока ни одно поле не изменилось, — разметка остаётся, и фокус на кнопках не теряется; @sticky-change сообщает состояние.

    Billing profile
    All changes saved

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
placement"top" | "bottom" | undefined"top"Край, к которому прилипает панель.
offsetstring | number | undefinedundefinedОтступ от края в прилипшем состоянии. Число — пиксели, строка — как есть (`4rem`, `var(--gr-navbar-height)`, `calc(…)`). Общий отступ для группы панелей задаётся не пропом, а токеном `--gr-affix-offset` на контейнере или на `:root`: наблюдатель читает вычисленный стиль и подхватывает его так же, как значение пропа.
disabledboolean | undefinedfalseПрилипание выключено: разметка та же, `sticky` не ставится. Именно проп, а не `v-if` снаружи: размонтирование убило бы фокус на кнопке панели и состояние вложенных контролов.

Слоты

SlotTypeОписание
default{ stuck: boolean; }Содержимое панели. `stuck` позволяет менять его в прилипшем виде.

События

EventСигнатураОписание
stickyChange(event: "stickyChange", stuck: boolean): void—

Методы

MethodsСигнатураОписание
stuckbooleanПрилипла ли панель сейчас — то же состояние, что в слоте и в `data-stuck`.
remeasure() => voidПеречитать отступ и пересобрать наблюдателя: `vh` после ресайза, смена высоты шапки.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrAffixProps, GrAffixEmits, GrAffixPlacement } from '@feugene/granularity/components/GrAffix'

Файлdist/types/components/GrAffix/GrAffix.vue.d.ts

GrAffixPropsinterface

GrAffix — панель, прилипающая к краю при прокрутке: заголовок раздела, оглавление, кнопки «Сохранить/Отмена» под длинной формой. Позиционирует position: sticky; наблюдатель нужен только чтобы **узнать**, прилипла ли панель, — от этого зависят фон, тень и содержимое. Отлипшая панель стоит внутри формы и обязана выглядеть её частью, поэтому поверхность включается вместе с состоянием, а не всегда, как у GrNavbar и GrTable. Корень компонента — сама липкая коробка, а не обёртка вокруг неё: sticky зажат своим containing block, и обёртка высотой в коробку не дала бы ей ехать вовсе. Сентинел — сосед коробки, отмечающий её место в потоке.

interface GrAffixProps {
    /** Край, к которому прилипает панель. */
    placement?: GrAffixPlacement;
    /**
     * Отступ от края в прилипшем состоянии. Число — пиксели, строка — как есть
     * (`4rem`, `var(--gr-navbar-height)`, `calc(…)`).
     *
     * Общий отступ для группы панелей задаётся не пропом, а токеном
     * `--gr-affix-offset` на контейнере или на `:root`: наблюдатель читает
     * вычисленный стиль и подхватывает его так же, как значение пропа.
     */
    offset?: number | string;
    /**
     * Прилипание выключено: разметка та же, `sticky` не ставится.
     *
     * Именно проп, а не `v-if` снаружи: размонтирование убило бы фокус на кнопке
     * панели и состояние вложенных контролов.
     */
    disabled?: boolean;
}
GrAffixEmitsinterface
interface GrAffixEmits {
    /** Панель прилипла (`true`) или отлипла (`false`); шлётся только при смене состояния. */
    (e: 'stickyChange', stuck: boolean): void;
}

Файлdist/types/components/GrAffix/config.d.ts

grAffixConfigconst
const grAffixConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrAffix">;

Файлdist/types/components/GrAffix/affixState.d.ts

GrAffixPlacementtype
type GrAffixPlacement = typeof GR_AFFIX_PLACEMENTS[number];

Файлdist/types/components/GrAffix/safelist.d.ts

grAffixSafelistconst
const grAffixSafelist: string[];

Файлdist/types/components/GrAffix/index.d.ts

GrAffixInstancetype
type GrAffixInstance = ComponentExposed<typeof GrAffixComponent>;

Использование из прозы библиотеки

«Прилипло» вычисляется зондом, а не догадкой

Рядом с панелью встаёт невидимый сентинел высотой в пиксель — он отмечает место, где панель стояла бы, не будь она липкой, и раскладку не сдвигает: пиксель высоты гасится пикселем отрицательного поля. За ним следит IntersectionObserver, у которого край корня поджат ровно на отступ прилипания, — тогда граница корня и есть линия, пересечение которой означает «прилипло».

Считается это по прямоугольникам записи, а не по её isIntersecting: тот одинаково равен false и когда панель ещё не доехала до края, и когда её containing block кончился и она уехала за него.

Цена сентинела ровно одна и видна в вёрстке: во flex- или grid-родителе он считается отдельным элементом — получает свой gap, а в гриде занимает ещё и ячейку, сдвигая соседей. Панель в таком родителе оборачивают обычным div.

Обёртка здесь не противоречит правилу выше: запрещена обёртка по высоте панели, а эта высоту получает от родителя — грид растягивает ячейку на весь ряд, — и потому остаётся containing block’ом, внутри которого панели есть куда ехать. То же даёт явная высота или align-self: stretch во flex-колонке.

Отступ можно задать группе, а не каждой панели

Проп offset — это данные экземпляра: «эта панель стоит под шапкой в 112 пикселей». Общий отступ для всех панелей раздела задаётся не пропом, а токеном на контейнере или на :root:

css
:root { --gr-affix-offset: 112px; }

Наблюдатель читает вычисленный стиль, а не значение пропа, поэтому каскад, 4rem, var(--gr-navbar-height) и calc(…) работают одинаково и одинаково подхватываются. Исключение из общего правила задаётся пропом на конкретной панели.

Отступ, изменившийся без смены пропа — после ресайза у единиц vh, после смены высоты шапки, — перечитывается методом remeasure() через ref на компонент.

Поверхность появляется вместе с прилипанием

Отлипшая панель стоит внутри формы и обязана выглядеть её частью, поэтому фон и тень включаются только в прилипшем виде — в отличие от GrNavbar и заголовка GrTable, которые непрозрачны всегда.

Фон обязателен: без него уезжающее под панель содержимое просвечивает насквозь. Отделяет её тень, а не рамка: рамка добавляет высоту и сдвинула бы раскладку ровно в момент прилипания — страница дёрнулась бы на пиксель.

Из тех же соображений панель обязана быть во всю ширину своего скроллпорта. Отступ, заданный на прокручиваемом блоке, оставляет поля по бокам от неё — и в этих просветах видно уезжающее содержимое. Отступы поэтому живут на содержимом: внутри самой панели и на её соседях, а не на общем контейнере.

Панель внутри карточки берёт её поверхность одной строкой:

vue
<GrAffix style="--gr-affix-bg: var(--gr-card)">
  …
</GrAffix>

Совсем без фона — --gr-affix-bg: transparent; --gr-affix-shadow: none.

Слой локальный, и это не упущение

Панель садится на z-index: 10 внутри своего stacking-контекста, а не на ступень общей шкалы. Шаг между слоями шкалы оставлен приложению ровно под такой случай («свой sticky-хедер под дропдауном»), а глобального ответа на вопрос «выше или ниже нижней навигации» у липкой панели нет — он зависит от приложения. Нужен слой шкалы: --gr-affix-z: var(--gr-z-navbar).

Несколько панелей друг под другом

Две верхние панели в одном скроллере прилипают на одном отступе и накладываются: автоматической стопки нет — она требовала бы замера высот на каждом кадре. Второй панели отступ задаётся вручную: :offset="112 + высотаПервой". При равном слое поверх окажется та, что позже в разметке.

Когда не липнет

position: sticky молча перестаёт работать, если между панелью и настоящим скроллером есть предок с overflow, отличным от visible: он сам становится скроллпортом, а прокручивать его нечем. Сборка при этом зелёная, класс в разметке стоит, и отличить это от собственной ошибки без DevTools нельзя — поэтому в dev-режиме компонент называет виновного предка в консоли.

Так же тихо прилипание ломают contain: paint, contain: strict | content и content-visibility: auto у предка. Их компонент не проверяет: разбор значений contain дал бы ложные срабатывания дороже пропущенного случая. Панель не липнет, а предупреждения нет — смотреть в первую очередь на них.

Границы
  • горизонтального прилипания нет. Края только top и bottom; закреплённая колонка — это GrDataTable, и там она сделана иначе.