Raccourci clavier : Ctrl + K
Commencer

Groupe de champs

Réunit deux contrôles de formulaire ou plus en un seul objet : un indicatif pays contre un numéro de téléphone, un champ de recherche contre son bouton. Les bordures partagées se fondent en un seul trait et seules les deux extrémités de la rangée restent arrondies.

Utilisation

vue
Phone number

Pick the country code, then type the rest

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox, VInput, VInputGroup } from 'vectis-ui'

const code = ref('+33')
const number = ref('')

const codes = [
  { value: '+33', label: '+33 France' },
  { value: '+32', label: '+32 Belgium' },
  { value: '+41', label: '+41 Switzerland' },
  { value: '+1', label: '+1 United States' },
]
</script>

<template>
  <VInputGroup label="Phone number" hint="Pick the country code, then type the rest">
    <VCombobox v-model="code" :options="codes" aria-label="Country code" class="code" />
    <VInput v-model="number" type="tel" aria-label="Number" />
  </VInputGroup>
</template>

<style scoped>
.code {
  flex: 0 0 9rem;
}
</style>

Exemples

Plusieurs champs dans une rang�e

Une rangée prend autant de segments que nécessaire. Chaque segment portant un champ prend une part égale de ce qui reste, les autres gardant leur largeur naturelle.

vue
Amount
Address

The scheme, the name and the extension

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox, VInput, VInputGroup } from 'vectis-ui'

const amount = ref('1200')
const currency = ref('eur')
const currencies = [
  { value: 'eur', label: 'EUR' },
  { value: 'usd', label: 'USD' },
  { value: 'gbp', label: 'GBP' },
]

const scheme = ref('https')
const domain = ref('vectis-ui')
const tld = ref('com')
const schemes = [
  { value: 'https', label: 'https://' },
  { value: 'http', label: 'http://' },
]
const tlds = [
  { value: 'com', label: '.com' },
  { value: 'dev', label: '.dev' },
  { value: 'fr', label: '.fr' },
]
</script>

<template>
  <div class="column">
    <VInputGroup label="Amount">
      <VInput v-model="amount" aria-label="Amount" inputmode="decimal" />
      <VCombobox v-model="currency" :options="currencies" aria-label="Currency" class="currency" />
    </VInputGroup>

    <!-- A row is not limited to two: each segment holding a field takes an equal share,
         and the ones given a width of their own keep it. -->
    <VInputGroup label="Address" hint="The scheme, the name and the extension">
      <VCombobox v-model="scheme" :options="schemes" aria-label="Scheme" class="scheme" />
      <VInput v-model="domain" aria-label="Domain name" />
      <VCombobox v-model="tld" :options="tlds" aria-label="Extension" class="tld" />
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 30rem;
}
.currency {
  flex: 0 0 7rem;
}
.scheme {
  flex: 0 0 8rem;
}
.tld {
  flex: 0 0 6rem;
}
</style>

Un champ et son bouton

Un bouton solid ou soft couvre le bord partagé avec son propre fond, et un bouton outline en tone="neutral" dessine la même couleur de bordure que les champs. Un bouton ghost n'a aucun cadre : c'est la seule variante à éviter ici.

vue
A solid button
An outline button, in the border colour of the fields
<script setup lang="ts">
import { VButton, VIconButton, VInput, VInputGroup } from 'vectis-ui'
import { search } from 'vectis-ui/icons'
</script>

<template>
  <div class="column">
    <VInputGroup label="A solid button">
      <VInput type="search" aria-label="Search terms" placeholder="Search the archive" />
      <VIconButton :icon="search" variant="solid" tone="accent" label="Search" />
    </VInputGroup>

    <VInputGroup label="An outline button, in the border colour of the fields">
      <VInput type="search" aria-label="Search terms" placeholder="Search the archive" />
      <VButton variant="outline" tone="neutral" :icon-start="search">Search</VButton>
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Deux champs qui ouvrent un panneau

Chaque panneau est ancré à la boîte de son propre champ, il s'ouvre donc sous le segment auquel il appartient et non sous le groupe. Les boutons que contient un panneau ne sont pas des segments de la rangée et gardent la taille que leur composant leur a donnée.

vue
Scheduled for

The date, then the time of day

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput, VInputGroup, VTimeInput } from 'vectis-ui'

const date = ref<string | null>('2026-09-21')
const time = ref<string | null>('09:30')
</script>

<template>
  <!-- Two anchored panels in one row. Each is anchored to its own field's box rather
       than to the group, so each opens under the segment it belongs to. -->
  <VInputGroup label="Scheduled for" hint="The date, then the time of day">
    <VDateInput v-model="date" show-picker aria-label="Date" />
    <VTimeInput v-model="time" show-picker aria-label="Time" class="time" />
  </VInputGroup>
</template>

<style scoped>
.time {
  flex: 0 0 13rem;
}
</style>

Nommer la rangée et ses segments

Le groupe rend un seul label et un seul hint pour toute la rangée. Chaque segment demande alors son propre aria-label, et celui qui apporte son label fait l'objet d'un avertissement en développement.

vue
Budget
<script setup lang="ts">
import { VButton, VInput, VInputGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <!-- A visible label names the row, and each segment takes an aria-label of its own. -->
    <VInputGroup label="Budget">
      <VInput aria-label="Amount" placeholder="0.00" />
      <VInput aria-label="Currency" placeholder="EUR" class="currency" />
    </VInputGroup>

    <!-- No visible label: the group is named by aria-label instead. -->
    <VInputGroup aria-label="Search the archive">
      <VInput type="search" aria-label="Search terms" placeholder="Search the archive" />
      <VButton variant="solid" tone="accent">Search</VButton>
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 24rem;
}

.currency {
  flex: 0 0 6rem;
}
</style>

Largeurs

Les proportions se posent sur le segment lui-même, par une classe ou un flex en ligne. Une règle de votre application n'est pas en couche, elle l'emporte donc sur la part distribuée par le groupe.

vue
Equal shares
A fixed currency field
<script setup lang="ts">
import { VCombobox, VInput, VInputGroup } from 'vectis-ui'

const currencies = [
  { value: 'eur', label: 'EUR' },
  { value: 'usd', label: 'USD' },
  { value: 'gbp', label: 'GBP' },
]
</script>

<template>
  <div class="column">
    <!-- Left alone, the two fields share the row equally. -->
    <VInputGroup label="Equal shares">
      <VCombobox :options="currencies" model-value="eur" aria-label="Currency" />
      <VInput aria-label="Amount" />
    </VInputGroup>

    <!-- An inline flex on the segment fixes its width; the other one takes the rest. -->
    <VInputGroup label="A fixed currency field">
      <VCombobox :options="currencies" model-value="eur" aria-label="Currency" class="currency" />
      <VInput aria-label="Amount" />
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}

.currency {
  flex: 0 0 7rem;
}
</style>

Taille et densité

size et compact se posent sur la rangée et atteignent chaque segment, en l'emportant sur ce qu'un segment demande pour lui-même.

vue
sm
md
lg
md, compact
<script setup lang="ts">
import { VCombobox, VInput, VInputGroup } from 'vectis-ui'

const sizes = ['sm', 'md', 'lg'] as const

const currencies = [
  { value: 'eur', label: 'EUR' },
  { value: 'usd', label: 'USD' },
]
</script>

<template>
  <div class="column">
    <VInputGroup v-for="size in sizes" :key="size" :size="size" :label="size">
      <VCombobox :options="currencies" model-value="eur" aria-label="Currency" class="currency" />
      <VInput aria-label="Amount" />
    </VInputGroup>

    <VInputGroup size="md" compact label="md, compact">
      <VCombobox :options="currencies" model-value="eur" aria-label="Currency" class="currency" />
      <VInput aria-label="Amount" />
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}

.currency {
  flex: 0 0 7rem;
}
</style>

États

Un groupe sans aucune prop de forme laisse chaque segment exactement tel qu'il était. disabled s'ajoute au lieu de remplacer : un segment coupé de lui-même le reste sous une rangée qui ne dit rien.

vue
The whole row switched off

Nothing here can be reached

One segment disabled on its own
A segment in error
<script setup lang="ts">
import { VButton, VInput, VInputGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VInputGroup disabled label="The whole row switched off" hint="Nothing here can be reached">
      <VInput aria-label="Amount" model-value="1200" />
      <VButton variant="solid" tone="accent">Convert</VButton>
    </VInputGroup>

    <VInputGroup label="One segment disabled on its own">
      <VInput aria-label="Amount" model-value="1200" />
      <VInput disabled aria-label="Converted amount" model-value="1284.50" />
    </VInputGroup>

    <VInputGroup label="A segment in error">
      <VInput invalid aria-label="Email" model-value="not-an-address" />
      <VButton variant="outline" tone="neutral">Check</VButton>
    </VInputGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDéfaut
labelstringaucune
Le libellé au-dessus de la rangée, rendu une seule fois pour tous ses segments et utilisé comme nom accessible du groupe. Un segment qui porte le sien se retrouve décalé, nommez donc chacun d'eux avec aria-label.
hintstringaucune
Une ligne d'aide sous la rangée, rattachée au groupe pour que les technologies d'assistance la lisent avec le libellé.
sizeInputGroupSize'sm' | 'md' | 'lg'aucune
La hauteur que prend chaque segment, quelle que soit celle qu'il nomme pour lui-même : une rangée de contrôles de deux hauteurs cesse de se lire comme un seul objet. Absente, chaque segment garde la sienne.
compactbooleanaucune
Retire 4px à la hauteur de chaque segment, comme compact le fait sur un champ isolé.
disabledbooleanaucune
Rend toute la rangée inutilisable. Cela s'ajoute à ce que dit chaque segment au lieu de le remplacer : un segment désactivé de lui-même le reste sous une rangée qui ne dit rien.

Slots

SlotType
default{}
Les champs et les boutons à joindre. Chacun est un segment de la rangée.