User clicks Release dropdown
↓
[Dropdown opens, shows: release-v1.0, release-v1.1, release-v2.0]
↓
User clicks "release-v1.0"
↓
bits-ui Select.Root detects selection
↓
bits-ui updates internal `value` prop to "release-v1.0"
↓
Svelte's `bind:value` syncs to component state
↓
createForm.releaseId = "release-v1.0" ✅ STRING VALUE
↓
Form submitted with correct data to API
SelectWrapper.svelte (wrapper)
↓
└─→ Exposes: bind:value={formData.releaseId}
bits-ui SelectPrimitive.Root (library)
↓
└─→ type="single" → Single-select mode
└─→ bind:value → Bindable value prop
└─→ Handles dropdown UI and selection logic
Result: formData.releaseId = "string-value" ✅
Stages.svelte
↓
<Select bind:value={createForm.releaseId}>
↓
SelectWrapper.svelte (custom, 7 lines)
↓
<SelectPrimitive.Root type="single" bind:value>
↓
bits-ui's native Select implementation
- Manages dropdown state
- Handles mouse/keyboard events
- Maintains value as string
↓
<SelectTrigger>, <SelectContent>, <SelectItem>
↓
Result: formData.releaseId = "my-release"
Before Fix (Broken):
Release selection → bits-ui → ??? → createForm.releaseId = ["release"] ❌
After Fix (Working):
Release selection → bits-ui.Root
↓
type="single" (enforces string)
↓
bind:value (Svelte binding)
↓
createForm.releaseId = "release" ✅
bits-ui Select.Root Properties:
┌─────────────────────────────────────────────────┐
│ type = "single" │
│ └─→ AUTOMATICALLY handles: STRING values │
│ │
│ type = "multiple" │
│ └─→ AUTOMATICALLY handles: ARRAY values │
│ │
│ bind:value │
│ └─→ SUPPORTS both via Svelte's binding │
└─────────────────────────────────────────────────┘
Our wrapper just exposes this:
type="single" + bind:value = Single-select string dropdown ✅
Old Approach:
- Try to intercept with onValueChange callback
- Attempt to extract value from bits-ui response
- Add custom conversion logic
- Result: Confused state management, unexpected types
New Approach:
- Use bits-ui's native type="single" behavior
- Use Svelte's native bind:value binding
- Let bits-ui handle the complexity
- Result: Clean state management, correct types
SelectWrapper.svelte does ONE thing:
Take bits-ui's Select.Root
↓
Set type="single" (enforces single-select)
↓
Expose via bind:value (Svelte binding)
↓
Pass through all other props
↓
That's it! 7 lines, no complexity.
<script>
let { value = $bindable(''), children, ...restProps } = $props()
function handleSelectionChange(selected: any) {
if (selected !== undefined && selected !== null) {
value = selected // ← Trying to extract from complex object
}
}
</script>
<SelectPrimitive.Root
type="single"
onValueChange={handleSelectionChange} // ← Custom callback
{...restProps}
>
{@render children?.()}
</SelectPrimitive.Root><script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'
let { value = $bindable(''), children, ...restProps } = $props()
</script>
<SelectPrimitive.Root type="single" bind:value {...restProps}>
{@render children?.()}
</SelectPrimitive.Root>Input: User selects "release-v1.0" from dropdown
↓
Processing: bits-ui's type="single" + bind:value
↓
Output: createForm.releaseId = "release-v1.0"
↓
Format: String ✅
Type: Correct for API ✅
Count: Single value ✅
Behavior: Single-select only ✅
This is the complete, correct solution.