GrFormSection
Groups related form fields into a clear, meaningful section.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the form is long — “Contacts”, “Delivery”, “Payment”: the sections give structure and points of navigation;
- the section is needed by a blind user — the heading renders as an
h2…h6tag, and the form is walked by headings; - the section has actions — “Add”, “Reset” in the right part of the header;
- the section needs an explanation — a description under the heading rather than a hint at every field.
When to take something else
| Need | Take |
|---|---|
| Rules and submission | GrForm |
| The wrapper of a single field | GrFormField |
| The sections are switched rather than following one another | GrTabs |
| The section collapses | GrCollapse |
| Just a card is needed | GrCard |
The heading is real
A blind user walks a long form by headings, so the heading of a section renders as an
h2…h6 tag rather than as bold text:
<GrFormSection title="Profile" :heading-level="2" description="Public data">
<GrFormField label="Name"><GrInput v-model="name" /></GrFormField>
</GrFormSection>
headingLevel is 3 by default and is read from GrConfigProvider
(componentDefaults.GrFormSection.headingLevel) — the level is set once for the whole
application, if the form has a page structure of its own.
A landmark on request
The root is a <section>, but without a name: a named section becomes a region
landmark, and five sections of a form would give five regions in the overview of a screen
reader. The structure is carried by the heading.
Where a section really is large and it is worth jumping to it as to a region, switch
landmark on — the name will come from the heading:
<GrFormSection landmark title="Security" />
The description is linked to the section through aria-describedby in both modes.
The slots
| Slot | What it replaces |
|---|---|
title | the heading as a whole — a link, a badge, a counter |
description | the description as a whole |
actions | the right part of the header: “Add”, “Reset” |
default | the content of the section |
The header is a single row: the heading with the description on the left, the actions on the right; on a narrow screen the actions wrap under the heading. Without a heading, a description and actions the header is not rendered at all.
The border and collapsing
There are deliberately no bordered/collapsible props — that is composition, not a
setting:
<GrCard class="p-4">
<GrFormSection title="Profile" />
</GrCard>
Collapsing is given by GrCollapse — it has the same headingLevel, so
the structure of the headings does not break.
Playground 3
Loading…
<GrFormSection />Install
npm i @feugene/granularityImport
import { GrFormSection } from '@feugene/granularity/components/GrFormSection'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
title | string | undefined | undefined | — |
description | string | undefined | undefined | — |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | The level of the heading to fit the structure of the page. Unset — it comes from `GrConfigProvider`, otherwise `3`. |
landmark | boolean | undefined | false | Declare the section a `region` landmark. Off by default: five sections of a form would give five landmarks and clutter the overview, while the structure is carried by the heading anyway. It is worth switching on where the section really is large and it is worth jumping to it as to a region. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The content of the section. |
title | any | The heading as a whole — instead of the `title` string. |
description | any | The description as a whole — instead of the `description` string. |
actions | any | The actions in the right part of the header: "Add", "Reset". |
Examples 5
Heading level, actions and slots
Участники проекта 2
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrButton, GrCard, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'
const members = ref([
{ id: 1, email: '[email protected]' },
{ id: 2, email: '[email protected]' },
])
function addMember(): void {
members.value = [...members.value, { id: Date.now(), email: '' }]
}
</script>
<template>
<GrCard class="p-4">
<!-- Заголовок секции — настоящий `h4`, поэтому форма обходится по заголовкам. -->
<GrFormSection
title="Участники проекта"
description="Приглашения уходят на почту сразу после сохранения."
:heading-level="4"
>
<template #title>
Участники проекта
<GrBadge tone="neutral">
{{ members.length }}
</GrBadge>
</template>
<template #actions>
<GrButton variant="outline" size="sm" @click="addMember">
Добавить
</GrButton>
</template>
<div class="grid gap-3">
<GrFormField v-for="(member, index) in members" :key="member.id" :label="`Участник ${index + 1}`">
<GrInput v-model="member.email" type="email" placeholder="[email protected]" />
</GrFormField>
</div>
</GrFormSection>
</GrCard>
</template>Section heading with profile fields
Team profile
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrFormSection, GrInput, GrTextarea } from '@feugene/granularity'
const teamName = ref('Platform operations')
const summary = ref('Coordinates deployments, release notes and service health updates.')
</script>
<template>
<GrFormSection
title="Team profile"
description="Use `GrFormSection` when a group of fields needs shared title and supporting copy."
>
<div class="grid gap-4 md:grid-cols-2">
<GrFormField label="Team name" for-id="team-name">
<GrInput id="team-name" v-model="teamName" placeholder="Operations" />
</GrFormField>
<GrFormField label="Summary" for-id="team-summary" class="md:col-span-2">
<GrTextarea id="team-summary" v-model="summary" :rows="4" />
</GrFormField>
</div>
</GrFormSection>
</template>Grouped controls inside one section
Notification routing
<script setup lang="ts">
import { ref } from 'vue'
import { GrCheckbox, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'
const channel = ref('release-updates')
const includeStakeholders = ref(true)
const requireApproval = ref(false)
</script>
<template>
<GrFormSection
title="Notification routing"
description="Section wrappers keep longer forms readable when fields are grouped by intent."
>
<div class="grid gap-4">
<GrFormField label="Slack channel" for-id="notify-channel">
<GrInput id="notify-channel" v-model="channel" placeholder="release-updates" />
</GrFormField>
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrCheckbox v-model="includeStakeholders">
Include business stakeholders in the launch message
</GrCheckbox>
<GrCheckbox v-model="requireApproval">
Require manual approval before notifications are sent
</GrCheckbox>
</div>
</div>
</GrFormSection>
</template>Stacked multi-section flow
Ownership
Operational assets
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'
const owner = ref('[email protected]')
const runbook = ref('docs/runbooks/incident-handoff')
</script>
<template>
<div class="grid gap-6">
<GrFormSection title="Ownership">
<GrFormField label="Primary owner" for-id="incident-owner">
<GrInput id="incident-owner" v-model="owner" placeholder="[email protected]" />
</GrFormField>
</GrFormSection>
<GrFormSection title="Operational assets" description="Multiple sections can be stacked to create a light-weight form page skeleton.">
<div class="grid gap-4">
<GrFormField label="Runbook path" for-id="runbook-path">
<GrInput id="runbook-path" v-model="runbook" placeholder="docs/runbooks/..." />
</GrFormField>
<div class="flex flex-wrap gap-2">
<GrBadge tone="success" radius="round">Owner assigned</GrBadge>
<GrBadge tone="info" radius="round">Runbook linked</GrBadge>
</div>
</div>
</GrFormSection>
</div>
</template>Bordered & rounded-border sections
Bordered section
Rounded-border section
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrFormSection, GrInput, GrTextarea } from '@feugene/granularity'
const projectName = ref('Granularity')
const apiToken = ref('')
const notes = ref('Rotate the token every 90 days.')
</script>
<template>
<div class="grid gap-6">
<!-- Прямоугольная рамка: класс проходит на корневой <section> секции. -->
<GrFormSection
title="Bordered section"
description="Border via utility classes — GrFormSection forwards `class` to its root <section>."
class="border border-[var(--gr-brd)] p-5"
>
<GrFormField label="Project name" for-id="bordered-project">
<GrInput id="bordered-project" v-model="projectName" placeholder="Acme" />
</GrFormField>
</GrFormSection>
<!-- Скруглённая рамка: те же классы + rounded-2xl и мягкая подложка. -->
<GrFormSection
title="Rounded-border section"
description="Same wrapper with rounded corners and a card surface."
class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-5"
>
<div class="grid gap-4">
<GrFormField label="API token" for-id="rounded-token">
<GrInput id="rounded-token" v-model="apiToken" placeholder="sk_live_…" />
</GrFormField>
<GrFormField label="Notes" for-id="rounded-notes">
<GrTextarea id="rounded-notes" v-model="notes" :rows="3" />
</GrFormField>
</div>
</GrFormSection>
</div>
</template>