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
<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
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.
<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 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.
<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.
<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.
<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.
<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.
<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.
<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
Slots