
Svelte 的 bind:group 无法跨组件工作,因此多个独立的自定义 RadioButton 组件无法自动互斥选中;需改用受控模式(checked + on:change)或通过上下文(Context)由父级 RadioGroup 统一管理状态。
svelte 的 `bind:group` 无法跨组件工作,因此多个独立的自定义 radiobutton 组件无法自动互斥选中;需改用受控模式(`checked` + `on:change`)或通过上下文(context)由父级 radiogroup 统一管理状态。
在构建可复用、样式完全自定义的 Svelte 单选按钮()时,一个常见误区是直接在子组件内使用 bind:group={checked}——这会导致多个实例同时响应并显示为“已选中”,破坏单选逻辑。根本原因在于:Svelte 官方明确说明 bind:group 仅在同一个 Svelte 组件内部的 元素间生效(详见文档)。当每个 是独立组件时,它们各自渲染的 并不共享同一作用域,bind:group 实际上分别绑定到各自的 checked 属性,而非一个全局组值。
✅ 正确解法有两种主流实践:
方案一:受控组件(推荐,简洁清晰)
将 RadioButton 改为纯展示 + 事件驱动,由父组件统一管理 selectedValue,并通过 on:change 显式更新:
<!-- RadioButton.svelte -->
<script>
import { toKebabCase } from "$lib/utils.js";
import Icon from "$lib/components/Icon.svelte";
export let name = '';
export let value = '';
export let checked = false; // 只读状态,不导出为双向绑定
export let classNames = '';
function handleChange() {
// 触发自定义事件,交由父组件决定是否更新
dispatch('change', { value });
}
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script><label for="{toKebabCase(value)}" class="relative flex flex-row items-center {classNames}">
<input type="radio" id="{toKebabCase(value)}" name="{name}" value="{value}" class="absolute opacity-0 w-0 h-0" on:change="{handleChange}"><span class="absolute cursor-pointer">
{#if checked}
<icon type="radioButtonChecked" classnames="h-6 w-6 fill-gs-teal"></icon>
{:else}
<icon type="radioButton" classnames="h-6 w-6 fill-white stroke-gray-400"></icon>
{/if}
</span>
<span class="ml-3"><slot></slot></span>
</label>
父组件使用方式(✅ 正确):
<!-- +Page.svelte -->
<script>
let radioValue = 'db1';
</script><radiobutton name="database" value="db1" checked on:change="{(e)"> radioValue = e.detail.value}
/>
<radiobutton name="database" value="db2" checked on:change="{(e)"> radioValue = e.detail.value}
/></radiobutton></radiobutton>
✅ 优势:逻辑透明、无隐式依赖;✅ 避免 DOM
name冲突风险;✅ 完全兼容 SSR 和无障碍(ARIA)。
方案二:上下文协调(适合复杂表单)
若需更高阶封装(如支持 required、disabled 批量控制、键盘导航等),可创建 包裹组件,利用 setContext / getContext 在后代间共享状态与更新函数:
<!-- RadioButtonGroup.svelte -->
<script>
import { setContext, onMount } from 'svelte';
import { writable } from 'svelte/store';
export let name = '';
export let bind:value;
// 创建可写 store 管理当前选中值
const selected = writable(bind:value || null);
$: $selected = bind$value; // 同步 prop 变更
setContext('radio-group', {
name,
selected,
select: (val) => selected.set(val)
});
</script><slot></slot>
子组件 RadioButton.svelte 内注入上下文并响应:
<script>
import { getContext } from 'svelte';
import { get } from 'svelte/store';
const { name, selected, select } = getContext('radio-group');
export let value = '';
$: isChecked = get(selected) === value;
</script><label class="flex items-center">
<input type="radio" name="{name}" value="{value}" bind:checked="{isChecked}" on:change="{()"> select(value)}
/>
<span class="ml-2"><slot></slot></span>
</label>
使用方式:
<radiobuttongroup name="database" bind:value="{radioValue}"><radiobutton value="db1">Database 1</radiobutton><radiobutton value="db2">Database 2</radiobutton></radiobuttongroup>
⚠️ 注意事项:
- 切勿在自定义组件中对
bind:group使用跨组件变量(如bind:group={checked}),这是根本性误用; - 自定义
时建议将原生设为opacity-0或w-0 h-0,保留语义与可访问性,视觉元素用替代; - 所有
name属性必须一致(且最好由父级统一提供),确保浏览器原生单选逻辑在必要时仍可降级生效; - 若使用方案一,务必为每个
提供唯一id(如示例中的toKebabCase(value)),便于for关联和屏幕阅读器识别。
综上,放弃 bind:group 跨组件幻想,拥抱显式状态流——这是 Svelte 响应式哲学的核心:清晰的数据流向比隐式魔法更可靠、更易调试、更具可维护性。










