GrSwitch
Берут, когда настройка включается сразу.
Когда брать
- настройка включается сразу — уведомления, тёмная тема, доступ по ссылке: кнопки «Сохранить» рядом нет;
- применение идёт на сервер —
loadingдержит состояние на время запроса, не давая переключить дважды; - состояние важнее выбора — «включено/выключено» читается быстрее пары вариантов;
- значение не булево —
valueзадаёт, что уходит наружу во включённом положении.
Когда взять другое
| Нужно | Берите |
|---|---|
| Значение уедет с отправкой формы | GrCheckbox |
| Вариантов два, но это режимы отображения | GrSegmented |
| Вариантов больше двух | GrRadioGroup |
| Значений несколько | GrCheckboxGroup |
Нативная форма
<GrSwitch v-model="notifications" name="notifications" value="on">
Email-уведомления
</GrSwitch>
Семантика чекбокса: включённый переключатель отправляет name=value,
выключенный не отправляется вовсе — сервер отличает «выкл» по отсутствию
ключа. Без name скрытого поля нет; form связывает переключатель с формой,
если он лежит вне неё.
Внутри <button> интерактивный контент невалиден, поэтому скрытое поле —
сосед кнопки, а корень компонента — фрагмент. class и прочие атрибуты
по-прежнему садятся на кнопку.
Загрузка
<GrSwitch :model-value="backup" :loading="syncing" @change="save" />
loading показывает спиннер в бегунке, помечает контрол aria-busy и блокирует
переключение, пока запрос в полёте. Что именно грузится, задаёт loadingText
(по умолчанию — ключ gr.switch.loading): aria-busy сам по себе часть
скринридеров не объявляет.
Подпись
Подпись приходит слотом по умолчанию, labelPosition="start" переносит её влево
от дорожки. Ряд именно разворачивается, а не переставляется в DOM: диктор читает
порядок узлов, и дорожка должна оставаться первой.
Без подписи имя обязателен задать иначе — ariaLabel или GrFormField, который
свяжет <label for> с кнопкой.
Состояния
| Проп | Что делает |
|---|---|
disabled | гасит контрол токенами --gr-disabled-bg / -brd / -fg; они перебивают и кастомные цвета дорожки |
readonly | состояние видно, но не меняется (aria-readonly) |
invalid, required | aria-invalid / aria-required; складываются с контекстом GrFormField |
Недоступность гасится фоном, а не opacity: прозрачность разбавляет
выверенные на AA токены текста и роняет контраст подписи.
События и клавиатура
update:modelValue и change эмитятся вместе. Экземпляр отдаёт focus() и
blur().
Клавиатура нативная: контрол — настоящая <button>, поэтому Space и Enter
переключают его силами браузера, а Tab обходит в общем порядке.
Оформление
size — xs…lg, читается из GrConfigProvider
(componentDefaults.GrSwitch.size). Цвет дорожки точечно задаётся пропами
activeBackgroundColor / inactiveBackgroundColor.
Playground 15
Загружается…
<GrSwitch />Установка
npm i @feugene/granularityИмпорт
import { GrSwitch } from '@feugene/granularity/components/GrSwitch'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValue | boolean | undefined | false | Значение переключателя. Необязательное: без `v-model` контрол рисуется выключенным — так же, как `GrCheckbox`. Обязательный проп заставлял бы заводить `ref` даже там, где переключатель presentational. |
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Только для чтения: состояние видно, но не переключается. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
loading | boolean | undefined | false | Идёт сохранение: бегунок показывает спиннер, переключение заблокировано. |
loadingText | string | undefined | undefined | i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. |
name | string | undefined | undefined | Имя поля для нативной отправки формы. Без него скрытое поле не рендерится. |
value | string | undefined | "on" | Значение, уходящее в форму во включённом состоянии. |
form | string | undefined | undefined | `id` формы, если переключатель лежит вне неё. |
labelPosition | "end" | "start" | undefined | "end" | Сторона подписи относительно дорожки. |
activeBackgroundColor | string | undefined | undefined | Кастомный цвет фона в активном состоянии. Если не задан — `var(--gr-primary)`. |
inactiveBackgroundColor | string | undefined | undefined | Кастомный цвет фона в неактивном состоянии. Если не задан — `var(--gr-muted)`. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Подпись переключателя. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: boolean] | — |
change | [value: boolean] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Примеры 4
Интерактивный конструктор переключателя
Соберите GrSwitch под ваш сценарий: меняйте состояние, size, подпись и локальные color overrides, сразу получая итоговый snippet.
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
GrFormField,
GrInput,
GrRadioGroup,
GrSwitch,
type GrSwitchSize,
} from '@feugene/granularity'
import CodeBlock from '../../../components/doc/CodeBlock.vue'
const checked = ref(true)
const disabled = ref(false)
const size = ref<GrSwitchSize>('md')
const label = ref('Email notifications')
const ariaLabel = ref('')
const activeBackgroundColor = ref('')
const inactiveBackgroundColor = ref('')
const sizeOptions = [
{ value: 'xs', label: 'XS' },
{ value: 'sm', label: 'SM' },
{ value: 'md', label: 'MD' },
{ value: 'lg', label: 'LG' },
] satisfies Array<{ value: GrSwitchSize, label: string }>
const switchText = computed(() => {
return label.value.trim() || 'Email notifications'
})
const resolvedAriaLabel = computed(() => {
return ariaLabel.value.trim() || undefined
})
const resolvedActiveBackgroundColor = computed(() => {
return activeBackgroundColor.value.trim() || undefined
})
const resolvedInactiveBackgroundColor = computed(() => {
return inactiveBackgroundColor.value.trim() || undefined
})
const previewSummary = computed(() => {
if (disabled.value)
return 'A disabled switch blocks state changes but keeps the visual context of the current setting.'
if (resolvedActiveBackgroundColor.value || resolvedInactiveBackgroundColor.value) {
return 'Local color overrides help embed the switch into a special scenario without changing global theme tokens.'
}
if (checked.value)
return 'In the enabled state the track uses the primary accent and works well for key feature toggles.'
return 'Pick the size, label and optional accessibility/color props to quickly assemble the switch contract you need.'
})
function escapeAttribute(value: string) {
return value.replaceAll('&', '&').replaceAll('"', '"')
}
const previewCode = computed(() => {
const attributes = [
`:model-value="${checked.value ? 'true' : 'false'}"`,
`size="${size.value}"`,
]
if (disabled.value)
attributes.push('disabled')
if (resolvedAriaLabel.value && resolvedAriaLabel.value !== switchText.value) {
attributes.push(`aria-label="${escapeAttribute(resolvedAriaLabel.value)}"`)
}
if (resolvedActiveBackgroundColor.value) {
attributes.push(`active-background-color="${escapeAttribute(resolvedActiveBackgroundColor.value)}"`)
}
if (resolvedInactiveBackgroundColor.value) {
attributes.push(`inactive-background-color="${escapeAttribute(resolvedInactiveBackgroundColor.value)}"`)
}
return ['<GrSwitch', ...attributes.map(attribute => ` ${attribute}`), '>', ` ${switchText.value}`, '</GrSwitch>'].join('\n')
})
</script>
<template>
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.15fr)_320px]">
<div class="grid gap-4">
<div
class="relative grid min-h-[280px] rounded-[24px] border border-dashed border-[var(--preview-brd)] bg-[image:var(--preview-surface)] p-6 pb-[72px]"
>
<div class="flex h-full flex-col items-center justify-center gap-4 text-center">
<div class="showcase-demo-caption text-xs">
Preview
</div>
<GrSwitch
:model-value="checked"
:disabled="disabled"
:size="size"
:aria-label="resolvedAriaLabel"
:active-background-color="resolvedActiveBackgroundColor"
:inactive-background-color="resolvedInactiveBackgroundColor"
@update:model-value="checked = $event"
>
{{ switchText }}
</GrSwitch>
<div class="pointer-events-none absolute inset-x-6 bottom-6 flex justify-center border-t border-dashed border-[var(--preview-brd)] pt-2">
<div class="showcase-demo-text max-w-[42ch] text-center text-sm">
{{ previewSummary }}
</div>
</div>
</div>
</div>
<CodeBlock :code="previewCode" language="vue" expanded title="Rendered snippet" />
</div>
<div class="showcase-demo-panel grid gap-4 rounded-[28px] border p-4 lg:p-5">
<div class="showcase-demo-title text-sm font-semibold">
Switch properties
</div>
<div class="grid gap-4">
<GrFormField label="Size">
<GrRadioGroup v-model="size" :options="sizeOptions" variant="button" size="sm" />
</GrFormField>
<GrFormField label="Label">
<GrInput
v-model="label"
placeholder="Email notifications"
aria-label="Switch label"
/>
</GrFormField>
<GrFormField label="Accessibility label">
<GrInput
v-model="ariaLabel"
placeholder="Used when the visible label is not enough"
aria-label="Switch accessibility label"
/>
</GrFormField>
<GrFormField label="Active background color">
<GrInput
v-model="activeBackgroundColor"
placeholder="#22c55e / var(--gr-primary)"
aria-label="Switch active background color"
/>
</GrFormField>
<GrFormField label="Inactive background color">
<GrInput
v-model="inactiveBackgroundColor"
placeholder="#e5e7eb / var(--gr-muted)"
aria-label="Switch inactive background color"
/>
</GrFormField>
</div>
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrSwitch v-model="checked" size="sm">
Checked
</GrSwitch>
<GrSwitch v-model="disabled" size="sm">
Disabled
</GrSwitch>
</div>
</div>
</div>
</template>Шкала размеров: от компактного до заметного
Один сценарий показывает, как переключатель масштабируется от компактных control bars до больших form-sections без изменения поведения.
<script setup lang="ts">
import { ref } from 'vue'
import { GrSwitch } from '@feugene/granularity'
const xsValue = ref(false)
const smValue = ref(false)
const mdValue = ref(true)
const lgValue = ref(true)
</script>
<template>
<div class="flex flex-wrap items-center gap-6">
<GrSwitch v-model="xsValue" size="xs">Extra small</GrSwitch>
<GrSwitch v-model="smValue" size="sm">Small</GrSwitch>
<GrSwitch v-model="mdValue" size="md">Medium</GrSwitch>
<GrSwitch v-model="lgValue" size="lg">Large</GrSwitch>
</div>
</template>Переключатели с подписью и выключенное состояние
Показываем, что label живёт в default slot, а disabled-режим одинаково корректно работает и для управляемого, и для статически включённого switch.
<script setup lang="ts">
import { ref } from 'vue'
import { GrSwitch } from '@feugene/granularity'
const notifications = ref(true)
const disabled = ref(false)
const syncing = ref(false)
const backup = ref(false)
function saveBackup(value: boolean): void {
syncing.value = true
window.setTimeout(() => {
backup.value = value
syncing.value = false
}, 1200)
}
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div class="grid gap-3">
<GrSwitch v-model="notifications" :disabled="disabled">
Email notifications
</GrSwitch>
<GrSwitch :model-value="true" disabled>
Always on
</GrSwitch>
<GrSwitch
:model-value="backup"
:loading="syncing"
label-position="start"
@change="saveBackup"
>
Automatic backup
</GrSwitch>
</div>
<div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrSwitch v-model="disabled" size="sm">
Disable labeled switch
</GrSwitch>
</div>
</div>
</template>Свои цвета включённого и выключенного
Фиксируем одну из ключевых интеграционных возможностей компонента: локально переопределять цвета трека без изменения глобальной темы.
<script setup lang="ts">
import { ref } from 'vue'
import { GrInput, GrSwitch } from '@feugene/granularity'
const enabled = ref(true)
const activeBackgroundColor = ref('#22c55e')
const inactiveBackgroundColor = ref('#e5e7eb')
</script>
<template>
<div class="grid gap-4">
<GrSwitch
v-model="enabled"
:active-background-color="activeBackgroundColor"
:inactive-background-color="inactiveBackgroundColor"
>
Custom colors
</GrSwitch>
<div class="grid gap-3 md:grid-cols-2">
<GrInput v-model="activeBackgroundColor" placeholder="#22c55e / var(--gr-primary)" />
<GrInput v-model="inactiveBackgroundColor" placeholder="#e5e7eb / var(--gr-muted)" />
</div>
</div>
</template>Доступность
- Паттерн APG
switch- Клавиши
Space,Enter— переключить (нативная<button role="switch">)