Input group
Joins two or more form controls into a single object: a country code against a phone number, a search field against its button. The shared borders melt into one line and only the two ends of the row stay rounded.
Usage
<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>Examples
A row takes as many segments as needed. Every segment holding a field takes an equal share of what is left, the others keeping their natural width.
<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>A solid or soft button covers the shared edge with its own background, and an outline button in tone="neutral" draws the same border colour as the fields. A ghost button has no frame at all, so it is the one variant to avoid here.
<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>Two fields that open a panel
Each panel is anchored to its own field's box, so it opens under the segment it belongs to rather than under the group. The buttons a panel contains are not segments of the row and keep the size their own component gave them.
<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>Naming the row and its segments
The group renders one label and one hint for the whole row. Each segment then needs an aria-label of its own, and one bringing its own label is warned about in development.
<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>Widths
Proportions are set on the segment itself, with a class or an inline flex. A consumer rule is unlayered, so it wins over the share the group hands out.
<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>Size and density
size and compact are set on the row and travel to every segment, winning over what a segment asks for itself.
<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>States
A group with none of the shape props set leaves every segment exactly as it was. disabled adds up instead of replacing, so a segment switched off on its own stays off under a row that says nothing.
<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