GrFormSection

Package: @feugene/granularitycoreGroup: forms

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 h2h6 tag, 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

NeedTake
Rules and submissionGrForm
The wrapper of a single fieldGrFormField
The sections are switched rather than following one anotherGrTabs
The section collapsesGrCollapse
Just a card is neededGrCard

The heading is real

A blind user walks a long form by headings, so the heading of a section renders as an h2h6 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

SlotWhat it replaces
titlethe heading as a whole — a link, a badge, a counter
descriptionthe description as a whole
actionsthe right part of the header: “Add”, “Reset”
defaultthe 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…

Code
<GrFormSection />

Install

npm i @feugene/granularity

Import

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

API

Props

PropTypedefaultDescription
titlestring | undefinedundefined
descriptionstring | undefinedundefined
headingLevel2 | 3 | 4 | 5 | 6 | undefinedundefinedThe level of the heading to fit the structure of the page. Unset — it comes from `GrConfigProvider`, otherwise `3`.
landmarkboolean | undefinedfalseDeclare 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

SlotTypeDescription
defaultanyThe content of the section.
titleanyThe heading as a whole — instead of the `title` string.
descriptionanyThe description as a whole — instead of the `description` string.
actionsanyThe actions in the right part of the header: "Add", "Reset".

Examples 5

Heading level, actions and slots

Участники проекта 2

Приглашения уходят на почту сразу после сохранения.

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

Use `GrFormSection` when a group of fields needs shared title and supporting copy.

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

Section wrappers keep longer forms readable when fields are grouped by intent.
Include business stakeholders in the launch message
Require manual approval before notifications are sent

Nested Groups
<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

Multiple sections can be stacked to create a light-weight form page skeleton.
Owner assignedRunbook linked

Stacked Flow
<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

Border via utility classes — GrFormSection forwards `class` to its root <section>.

Rounded-border section

Same wrapper with rounded corners and a card surface.

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

Component documentationAll components