Repository: melt-ui/melt-ui
Pull request #962 (2024-02-07 00:44:38)

Hidden input

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: ee5540248d04a613f6276f3049a76fdff4a24898

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name            | Type  |
| --------------- | ----- |
| @melt-ui/svelte | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/melt-ui/melt-ui/new/hidden-input?filename=.changeset/fuzzy-lemons-fetch.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0AHidden%20input%0A)


github-actions[bot]: <!-- refined-cf-pages-action:deployment-summary:melt-ui -->

###### built with [Refined Cloudflare Pages Action](https://github.com/AdrianGonz97/refined-cf-pages-action)
## ⚡ Cloudflare Pages Deployment
| Name | Status | Preview | Last Commit |
| :--- | :----- | :------ | :---------- |
| **melt-ui** | 🔨 Building ([View Log](https://github.com/melt-ui/melt-ui/actions/runs/7808207578)) |  | ee5540248d04a613f6276f3049a76fdff4a24898 |


Patch:
diff --git a/.changeset/curly-pandas-jump.md b/.changeset/curly-pandas-jump.md
new file mode 100644
index 00000000..cc44d9e7
--- /dev/null
+++ b/.changeset/curly-pandas-jump.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': patch
+---
+
+Standardize hidden inputs (closes #249)
diff --git a/.changeset/fast-badgers-report.md b/.changeset/fast-badgers-report.md
new file mode 100644
index 00000000..4df54bf1
--- /dev/null
+++ b/.changeset/fast-badgers-report.md
@@ -0,0 +1,5 @@
+---
+'@melt-ui/svelte': minor
+---
+
+Hidden Input: Dispatch change events (closes #690)
diff --git a/src/docs/previews/select/main/tailwind/index.svelte b/src/docs/previews/select/main/tailwind/index.svelte
index 231d65e5..f8160bfd 100644
--- a/src/docs/previews/select/main/tailwind/index.svelte
+++ b/src/docs/previews/select/main/tailwind/index.svelte
@@ -1,6 +1,6 @@
 <script lang="ts">
-	import { createSelect, melt } from '$lib/index.js';
 	import { Check, ChevronDown } from '$icons/index.js';
+	import { createSelect, melt } from '$lib/index.js';
 	import { fade } from 'svelte/transition';
 
 	const options = {
diff --git a/src/docs/utils/preview.ts b/src/docs/utils/preview.ts
index 7c27e562..5c47bb96 100644
--- a/src/docs/utils/preview.ts
+++ b/src/docs/utils/preview.ts
@@ -48,7 +48,6 @@ type CreatePreviewsObjectArgs = {
 async function createPreviewsObject({
 	component,
 	objArr,
-	fetcher,
 }: CreatePreviewsObjectArgs): Promise<PreviewObj> {
 	const returnedObj: PreviewObj = {};
 
@@ -119,7 +118,7 @@ function isMainPreviewComponent(builder: string, path: string): boolean {
 		regexMap.set(builder, new RegExp(regexPattern));
 	}
 
-	return regexMap.get(builder)!.test(path);
+	return !!regexMap.get(builder)?.test(path);
 }
 
 export async function getDocData(slug: string) {
diff --git a/src/lib/builders/accordion/create.ts b/src/lib/builders/accordion/create.ts
index a9bc2d57..3fc78ee2 100644
--- a/src/lib/builders/accordion/create.ts
+++ b/src/lib/builders/accordion/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	executeCallbacks,
@@ -58,7 +58,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 		return (key: string) => isSelected(key, $value);
 	});
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		returned: () => ({
 			'data-melt-id': meltIds.root,
 		}),
@@ -80,7 +80,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 		}
 	};
 
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		stores: value,
 		returned: ($value) => {
 			return (props: AccordionItemProps) => {
@@ -94,7 +94,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 		},
 	});
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [value, disabled],
 		returned: ([$value, $disabled]) => {
 			return (props: AccordionItemProps) => {
@@ -168,7 +168,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 		},
 	});
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [value, disabled, forceVisible],
 		returned: ([$value, $disabled, $forceVisible]) => {
 			return (props: AccordionItemProps) => {
@@ -202,7 +202,7 @@ export const createAccordion = <Multiple extends boolean = false>(
 		},
 	});
 
-	const heading = builder(name('heading'), {
+	const heading = makeElement(name('heading'), {
 		returned: () => {
 			return (props: AccordionHeadingProps) => {
 				const { level } = parseHeadingProps(props);
diff --git a/src/lib/builders/avatar/create.ts b/src/lib/builders/avatar/create.ts
index 4270527a..14be4549 100644
--- a/src/lib/builders/avatar/create.ts
+++ b/src/lib/builders/avatar/create.ts
@@ -1,5 +1,5 @@
 import {
-	builder,
+	makeElement,
 	effect,
 	isBrowser,
 	omit,
@@ -45,7 +45,7 @@ export const createAvatar = (props?: CreateAvatarProps) => {
 		}
 	});
 
-	const image = builder('avatar-image', {
+	const image = makeElement('avatar-image', {
 		stores: [src, loadingStatus],
 		returned: ([$src, $loadingStatus]) => {
 			const imageStyles = styleToString({
@@ -58,7 +58,7 @@ export const createAvatar = (props?: CreateAvatarProps) => {
 		},
 	});
 
-	const fallback = builder('avatar-fallback', {
+	const fallback = makeElement('avatar-fallback', {
 		stores: [loadingStatus],
 		returned: ([$loadingStatus]) => {
 			return {
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index d6102212..98406db8 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -276,7 +276,7 @@ export function createCalendar<
 	 * The root element of the calendar, capable of housing multiple grids/months
 	 * when using paged navigation.
 	 */
-	const calendar = builder(name(), {
+	const calendar = makeElement(name(), {
 		stores: [fullCalendarLabel, isInvalid, disabled, readonly, ids.calendar],
 		returned: ([$fullCalendarLabel, $isInvalid, $disabled, $readonly, $calendarId]) => {
 			return {
@@ -317,7 +317,7 @@ export function createCalendar<
 	 * the `calendarLabel` prop to 'Booking Date', the accessible heading will be 'Booking Date,
 	 * January 2021' for the same month and year.
 	 */
-	const heading = builder(name('heading'), {
+	const heading = makeElement(name('heading'), {
 		stores: [disabled],
 		returned: ([$disabled]) => {
 			return {
@@ -334,7 +334,7 @@ export function createCalendar<
 	 *
 	 * For more details about the structure of the month object, refer to {@link Month}.
 	 */
-	const grid = builder(name('grid'), {
+	const grid = makeElement(name('grid'), {
 		stores: [readonly, disabled],
 		returned: ([$readonly, $disabled]) => {
 			return {
@@ -354,7 +354,7 @@ export function createCalendar<
 	 * back by the number of months specified in the `numberOfMonths` prop.
 	 * In non-paged mode, it shifts the calendar back by one month.
 	 */
-	const prevButton = builder(name('prevButton'), {
+	const prevButton = makeElement(name('prevButton'), {
 		stores: [isPrevButtonDisabled],
 		returned: ([$isPrevButtonDisabled]) => {
 			const disabled = $isPrevButtonDisabled;
@@ -385,7 +385,7 @@ export function createCalendar<
 	 * specified in the `numberOfMonths` prop. If not using paged navigation, it
 	 * moves the calendar forward by one month.
 	 */
-	const nextButton = builder(name('nextButton'), {
+	const nextButton = makeElement(name('nextButton'), {
 		stores: [isNextButtonDisabled],
 		returned: ([$isNextButtonDisabled]) => {
 			const disabled = $isNextButtonDisabled;
@@ -416,7 +416,7 @@ export function createCalendar<
 	 * essential attributes and event handlers for accessibility
 	 * and interactivity.
 	 */
-	const cell = builder(name('cell'), {
+	const cell = makeElement(name('cell'), {
 		stores: [
 			isDateSelected,
 			isDateDisabled,
diff --git a/src/lib/builders/checkbox/create.ts b/src/lib/builders/checkbox/create.ts
index 03f95b17..3e33ba85 100644
--- a/src/lib/builders/checkbox/create.ts
+++ b/src/lib/builders/checkbox/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	disabledAttr,
 	executeCallbacks,
 	kbd,
@@ -32,7 +32,7 @@ export function createCheckbox(props?: CreateCheckboxProps) {
 	const checkedWritable = withDefaults.checked ?? writable(withDefaults.defaultChecked);
 	const checked = overridable(checkedWritable, withDefaults?.onCheckedChange);
 
-	const root = builder('checkbox', {
+	const root = makeElement('checkbox', {
 		stores: [checked, disabled, required],
 		returned: ([$checked, $disabled, $required]) => {
 			return {
@@ -68,7 +68,7 @@ export function createCheckbox(props?: CreateCheckboxProps) {
 		},
 	});
 
-	const input = builder('checkbox-input', {
+	const input = makeElement('checkbox-input', {
 		stores: [checked, name, value, required, disabled],
 		returned: ([$checked, $name, $value, $required, $disabled]) => {
 			return {
diff --git a/src/lib/builders/collapsible/create.ts b/src/lib/builders/collapsible/create.ts
index 25e2017a..7c0a264b 100644
--- a/src/lib/builders/collapsible/create.ts
+++ b/src/lib/builders/collapsible/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	omit,
@@ -30,7 +30,7 @@ export function createCollapsible(props?: CreateCollapsibleProps) {
 	const openWritable = withDefaults.open ?? writable(withDefaults.defaultOpen);
 	const open = overridable(openWritable, withDefaults?.onOpenChange);
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [open, disabled],
 		returned: ([$open, $disabled]) => ({
 			'data-state': $open ? 'open' : 'closed',
@@ -38,7 +38,7 @@ export function createCollapsible(props?: CreateCollapsibleProps) {
 		}),
 	});
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [open, disabled],
 		returned: ([$open, $disabled]) =>
 			({
@@ -64,7 +64,7 @@ export function createCollapsible(props?: CreateCollapsibleProps) {
 		([$open, $forceVisible]) => $open || $forceVisible
 	);
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [isVisible, disabled],
 		returned: ([$isVisible, $disabled]) => ({
 			'data-state': $isVisible ? 'open' : 'closed',
diff --git a/src/lib/builders/combobox/create.ts b/src/lib/builders/combobox/create.ts
index 1c36ee5d..4633b17f 100644
--- a/src/lib/builders/combobox/create.ts
+++ b/src/lib/builders/combobox/create.ts
@@ -2,7 +2,7 @@ import { useEscapeKeydown } from '$lib/internal/actions/index.js';
 import {
 	addEventListener,
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -44,7 +44,7 @@ export function createCombobox<
 	/* -------- */
 
 	/** Action and attributes for the text input. */
-	const input = builder(name('input'), {
+	const input = makeElement(name('input'), {
 		stores: [listbox.elements.trigger, inputValue],
 		returned: ([$trigger, $inputValue]) => {
 			return {
diff --git a/src/lib/builders/context-menu/create.ts b/src/lib/builders/context-menu/create.ts
index dd4a7b5f..dc50ea0e 100644
--- a/src/lib/builders/context-menu/create.ts
+++ b/src/lib/builders/context-menu/create.ts
@@ -2,7 +2,7 @@ import { usePopper } from '$lib/internal/actions/index.js';
 import {
 	FIRST_LAST_KEYS,
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	derivedVisible,
 	effect,
@@ -134,7 +134,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		activeTrigger: rootActiveTrigger,
 	});
 
-	const menu = builder(name(), {
+	const menu = makeElement(name(), {
 		stores: [isVisible, portal, ids.menu, ids.trigger],
 		returned: ([$isVisible, $portal, $menuId, $triggerId]) => {
 			// We only want to render the menu when it's open and has an active trigger.
@@ -236,7 +236,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
 		},
 	});
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [rootOpen, ids.trigger],
 		returned: ([$rootOpen, $triggerId]) => {
 			return {
diff --git a/src/lib/builders/date-field/create.ts b/src/lib/builders/date-field/create.ts
index 4743caf0..7f761e1f 100644
--- a/src/lib/builders/date-field/create.ts
+++ b/src/lib/builders/date-field/create.ts
@@ -13,7 +13,7 @@ import {
 } from '$lib/internal/helpers/date/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -62,6 +62,7 @@ import type {
 } from './_internal/types.js';
 import type { DateFieldEvents } from './events.js';
 import type { CreateDateFieldProps } from './types.js';
+import { createHiddenInput } from '../hidden-input/create.js';
 
 const defaults = {
 	isDateUnavailable: undefined,
@@ -81,7 +82,8 @@ const defaults = {
 
 type DateFieldParts = 'segment' | 'label' | 'hidden-input' | 'field' | 'validation';
 
-const { name } = createElHelpers<DateFieldParts>('dateField');
+const prefix = 'dateField';
+const { name } = createElHelpers<DateFieldParts>(prefix);
 
 const dateFieldIdParts = [
 	'field',
@@ -265,7 +267,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 		($allSegmentContent) => $allSegmentContent.obj
 	);
 
-	const label = builder(name('label'), {
+	const label = makeElement(name('label'), {
 		stores: [isInvalid, disabled, ids.label],
 		returned: ([$isInvalid, $disabled, $labelId]) => {
 			return {
@@ -294,7 +296,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 		},
 	});
 
-	const validation = builder(name('validation'), {
+	const validation = makeElement(name('validation'), {
 		stores: [isInvalid, ids.validation],
 		returned: ([$isInvalid, $validationId]) => {
 			const validStyle = styleToString({
@@ -309,26 +311,12 @@ export function createDateField(props?: CreateDateFieldProps) {
 		},
 	});
 
-	const hiddenInput = builder(name('hidden-input'), {
-		stores: [value, nameStore, disabled, required],
-		returned: ([$value, $nameStore, $disabled, $required]) => {
-			return {
-				name: $nameStore,
-				value: $value?.toString(),
-				'aria-hidden': 'true' as const,
-				hidden: true,
-				disabled: $disabled,
-				required: $required,
-				tabIndex: -1,
-				style: styleToString({
-					position: 'absolute',
-					opacity: 0,
-					'pointer-events': 'none',
-					margin: 0,
-					transform: 'translateX(-100%)',
-				}),
-			};
-		},
+	const hiddenInput = createHiddenInput({
+		prefix,
+		value: derived(value, ($value) => $value?.toString() ?? ''),
+		name: nameStore,
+		disabled,
+		required,
 	});
 
 	const fieldIdDeps = derived(
@@ -346,7 +334,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 	/**
 	 * The date field element which contains all the segments.
 	 */
-	const field = builder(name('field'), {
+	const field = makeElement(name('field'), {
 		stores: [value, isInvalid, disabled, readonly, fieldIdDeps],
 		returned: ([$value, $isInvalid, $disabled, $readonly, $ids]) => {
 			const describedBy = $value
@@ -420,7 +408,7 @@ export function createDateField(props?: CreateDateFieldProps) {
 		},
 	};
 
-	const segment = builder(name('segment'), {
+	const segment = makeElement(name('segment'), {
 		stores: [
 			segmentValues,
 			hourCycle,
diff --git a/src/lib/builders/date-picker/create.ts b/src/lib/builders/date-picker/create.ts
index 9efb5e82..9e52e512 100644
--- a/src/lib/builders/date-picker/create.ts
+++ b/src/lib/builders/date-picker/create.ts
@@ -5,7 +5,7 @@ import {
 } from '$lib/internal/helpers/date/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	effect,
 	omit,
 	toWritableStores,
@@ -75,7 +75,7 @@ export function createDatePicker(props?: CreateDatePickerProps) {
 		onOutsideClick: withDefaults.onOutsideClick,
 	});
 
-	const trigger = builder('popover-trigger', {
+	const trigger = makeElement('popover-trigger', {
 		stores: [popover.elements.trigger],
 		returned: ([$trigger]) => {
 			return {
diff --git a/src/lib/builders/date-range-field/create.ts b/src/lib/builders/date-range-field/create.ts
index aac3dc46..981a4593 100644
--- a/src/lib/builders/date-range-field/create.ts
+++ b/src/lib/builders/date-range-field/create.ts
@@ -9,7 +9,7 @@ import {
 } from '$lib/internal/helpers/date/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -174,7 +174,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		}
 	);
 
-	const label = builder(name('label'), {
+	const label = makeElement(name('label'), {
 		stores: [isInvalid, options.disabled, ids.label],
 		returned: ([$isInvalid, $disabled, $labelId]) => {
 			return {
@@ -215,7 +215,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		}
 	);
 
-	const field = builder(name('field'), {
+	const field = makeElement(name('field'), {
 		stores: [isCompleted, isInvalid, fieldIdDeps],
 		returned: ([$isCompleted, $isInvalid, $ids]) => {
 			const describedBy = $isCompleted
@@ -240,7 +240,7 @@ export function createDateRangeField(props?: CreateDateRangeFieldProps) {
 		},
 	});
 
-	const validation = builder(name('validation'), {
+	const validation = makeElement(name('validation'), {
 		stores: [isInvalid, ids.validation],
 		returned: ([$isInvalid, $validationId]) => {
 			const validStyle = styleToString({
diff --git a/src/lib/builders/date-range-picker/create.ts b/src/lib/builders/date-range-picker/create.ts
index c1e73c6c..6c89c640 100644
--- a/src/lib/builders/date-range-picker/create.ts
+++ b/src/lib/builders/date-range-picker/create.ts
@@ -5,7 +5,7 @@ import {
 } from '$lib/internal/helpers/date/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	effect,
 	omit,
 	toWritableStores,
@@ -87,7 +87,7 @@ export function createDateRangePicker(props?: CreateDateRangePickerProps) {
 
 	const placeholder = dateStore(rfPlaceholder, withDefaults.defaultPlaceholder ?? defaultDate);
 
-	const trigger = builder('popover-trigger', {
+	const trigger = makeElement('popover-trigger', {
 		stores: [popover.elements.trigger],
 		returned: ([$trigger]) => {
 			return {
diff --git a/src/lib/builders/dialog/create.ts b/src/lib/builders/dialog/create.ts
index 72bb7108..3b003499 100644
--- a/src/lib/builders/dialog/create.ts
+++ b/src/lib/builders/dialog/create.ts
@@ -1,7 +1,7 @@
 import { createFocusTrap, useEscapeKeydown, usePortal } from '$lib/internal/actions/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -100,7 +100,7 @@ export function createDialog(props?: CreateDialogProps) {
 		});
 	}
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [open],
 		returned: ([$open]) => {
 			return {
@@ -127,7 +127,7 @@ export function createDialog(props?: CreateDialogProps) {
 		},
 	});
 
-	const overlay = builder(name('overlay'), {
+	const overlay = makeElement(name('overlay'), {
 		stores: [isVisible],
 		returned: ([$isVisible]) => {
 			return {
@@ -162,7 +162,7 @@ export function createDialog(props?: CreateDialogProps) {
 		},
 	});
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [isVisible, ids.content, ids.description, ids.title],
 		returned: ([$isVisible, $contentId, $descriptionId, $titleId]) => {
 			return {
@@ -253,7 +253,7 @@ export function createDialog(props?: CreateDialogProps) {
 		},
 	});
 
-	const portalled = builder(name('portalled'), {
+	const portalled = makeElement(name('portalled'), {
 		stores: portal,
 		returned: ($portal) => ({
 			'data-portal': $portal ? '' : undefined,
@@ -279,21 +279,21 @@ export function createDialog(props?: CreateDialogProps) {
 		},
 	});
 
-	const title = builder(name('title'), {
+	const title = makeElement(name('title'), {
 		stores: [ids.title],
 		returned: ([$titleId]) => ({
 			id: $titleId,
 		}),
 	});
 
-	const description = builder(name('description'), {
+	const description = makeElement(name('description'), {
 		stores: [ids.description],
 		returned: ([$descriptionId]) => ({
 			id: $descriptionId,
 		}),
 	});
 
-	const close = builder(name('close'), {
+	const close = makeElement(name('close'), {
 		returned: () =>
 			({
 				type: 'button',
diff --git a/src/lib/builders/hidden-input/create.ts b/src/lib/builders/hidden-input/create.ts
new file mode 100644
index 00000000..274ba916
--- /dev/null
+++ b/src/lib/builders/hidden-input/create.ts
@@ -0,0 +1,60 @@
+import { createElHelpers, makeElement } from '$lib/internal/helpers/makeElement.js';
+import { readable } from 'svelte/store';
+import type { CreateHiddenInputProps } from './types.js';
+import { styleToString } from '$lib/internal/helpers/style.js';
+import { toReadableStores } from '$lib/internal/helpers/store/toReadableStores.js';
+import { omit } from '$lib/internal/helpers/object.js';
+import type { Readable } from 'svelte/motion';
+import { removeUndefined } from '$lib/internal/helpers/object.js';
+
+const defaults = {
+	prefix: '',
+	disabled: readable(false),
+	required: readable(false),
+	name: readable(undefined),
+} satisfies Partial<CreateHiddenInputProps>;
+
+export function createHiddenInput(props: CreateHiddenInputProps) {
+	const withDefaults = {
+		...defaults,
+		...removeUndefined(props),
+	} satisfies CreateHiddenInputProps;
+	const { name: elName } = createElHelpers(withDefaults.prefix);
+	const { value, name, disabled, required } = toReadableStores(omit(withDefaults, 'prefix'));
+	const nameStore = name as Readable<string | undefined>; // TODO: Remove this cast when types are fixed
+
+	const hiddenInput = makeElement(elName('hidden-input'), {
+		stores: [value, nameStore, disabled, required],
+		returned: ([$value, $name, $disabled, $required]) => {
+			return {
+				name: $name,
+				value: $value?.toString(),
+				'aria-hidden': 'true' as const,
+				hidden: true,
+				disabled: $disabled,
+				required: $required,
+				tabIndex: -1,
+				style: styleToString({
+					position: 'absolute',
+					opacity: 0,
+					'pointer-events': 'none',
+					margin: 0,
+					transform: 'translateX(-100%)',
+				}),
+			};
+		},
+		action: (node: HTMLInputElement) => {
+			// When value changes, emit a change event
+			const unsub = value.subscribe((newValue) => {
+				node.value = newValue;
+				node.dispatchEvent(new Event('change', { bubbles: true }));
+			});
+
+			return {
+				destroy: unsub,
+			};
+		},
+	});
+
+	return hiddenInput;
+}
diff --git a/src/lib/builders/hidden-input/index.ts b/src/lib/builders/hidden-input/index.ts
new file mode 100644
index 00000000..c7734bfb
--- /dev/null
+++ b/src/lib/builders/hidden-input/index.ts
@@ -0,0 +1,2 @@
+export * from './create.js';
+export * from './types.js';
diff --git a/src/lib/builders/hidden-input/types.ts b/src/lib/builders/hidden-input/types.ts
new file mode 100644
index 00000000..818c1836
--- /dev/null
+++ b/src/lib/builders/hidden-input/types.ts
@@ -0,0 +1,9 @@
+import type { Readable } from 'svelte/store';
+
+export type CreateHiddenInputProps = {
+	value: Readable<string>;
+	disabled?: Readable<boolean>;
+	name?: Readable<string | undefined> | undefined;
+	required?: Readable<boolean>;
+	prefix?: string;
+};
diff --git a/src/lib/builders/label/create.ts b/src/lib/builders/label/create.ts
index 6fa1fcb0..6a3e5384 100644
--- a/src/lib/builders/label/create.ts
+++ b/src/lib/builders/label/create.ts
@@ -1,9 +1,9 @@
-import { addMeltEventListener, builder } from '$lib/internal/helpers/index.js';
+import { addMeltEventListener, makeElement } from '$lib/internal/helpers/index.js';
 import type { MeltActionReturn } from '$lib/internal/types.js';
 import type { LabelEvents } from './events.js';
 
 export function createLabel() {
-	const root = builder('label', {
+	const root = makeElement('label', {
 		action: (node: HTMLElement): MeltActionReturn<LabelEvents['root']> => {
 			const mouseDown = addMeltEventListener(node, 'mousedown', (e) => {
 				if (!e.defaultPrevented && e.detail > 1) {
diff --git a/src/lib/builders/link-preview/create.ts b/src/lib/builders/link-preview/create.ts
index 9c8a6a74..5ab7d08b 100644
--- a/src/lib/builders/link-preview/create.ts
+++ b/src/lib/builders/link-preview/create.ts
@@ -1,7 +1,7 @@
 import { usePopper } from '$lib/internal/actions/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	derivedVisible,
 	effect,
@@ -110,7 +110,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 		}
 	) as WithGet<Readable<() => void>>;
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [open, ids.trigger, ids.content],
 		returned: ([$open, $triggerId, $contentId]) => {
 			return {
@@ -147,7 +147,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 
 	const isVisible = derivedVisible({ open, forceVisible, activeTrigger });
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [isVisible, portal, ids.content],
 		returned: ([$isVisible, $portal, $contentId]) => {
 			return {
@@ -258,7 +258,7 @@ export function createLinkPreview(props: CreateLinkPreviewProps = {}) {
 		},
 	});
 
-	const arrow = builder(name('arrow'), {
+	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
 		returned: ($arrowSize) => ({
 			'data-arrow': true,
diff --git a/src/lib/builders/listbox/create.ts b/src/lib/builders/listbox/create.ts
index 1692e170..705466a9 100644
--- a/src/lib/builders/listbox/create.ts
+++ b/src/lib/builders/listbox/create.ts
@@ -4,7 +4,6 @@ import {
 	addHighlight,
 	addMeltEventListener,
 	back,
-	builder,
 	createClickOutsideIgnore,
 	createElHelpers,
 	createTypeaheadSearch,
@@ -16,7 +15,6 @@ import {
 	generateId,
 	getOptions,
 	getPortalDestination,
-	hiddenInputAttrs,
 	isBrowser,
 	isElement,
 	isElementDisabled,
@@ -26,6 +24,7 @@ import {
 	isObject,
 	kbd,
 	last,
+	makeElement,
 	next,
 	noop,
 	omit,
@@ -43,8 +42,9 @@ import { safeOnMount } from '$lib/internal/helpers/lifecycle.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { dequal as deepEqual } from 'dequal';
 import { tick } from 'svelte';
-import { derived, get, writable, type Readable } from 'svelte/store';
+import { derived, get, readonly, writable, type Readable } from 'svelte/store';
 import { generateIds } from '../../internal/helpers/id.js';
+import { createHiddenInput } from '../hidden-input/create.js';
 import { createLabel } from '../label/create.js';
 import type { ListboxEvents } from './events.js';
 import type {
@@ -273,7 +273,7 @@ export function createListbox<
 	/* -------- */
 
 	/** Action and attributes for the text input. */
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [open, highlightedItem, disabled, ids.menu, ids.trigger, ids.label],
 		returned: ([$open, $highlightedItem, $disabled, $menuId, $triggerId, $labelId]) => {
 			return {
@@ -458,7 +458,7 @@ export function createListbox<
 	/**
 	 * Action and attributes for the menu element.
 	 */
-	const menu = builder(name('menu'), {
+	const menu = makeElement(name('menu'), {
 		stores: [isVisible, ids.menu],
 		returned: ([$isVisible, $menuId]) => {
 			return {
@@ -528,7 +528,7 @@ export function createListbox<
 	} = createLabel();
 	const { action: labelAction } = get(labelBuilder);
 
-	const label = builder(name('label'), {
+	const label = makeElement(name('label'), {
 		stores: [ids.label, ids.trigger],
 		returned: ([$labelId, $triggerId]) => {
 			return {
@@ -539,7 +539,7 @@ export function createListbox<
 		action: labelAction,
 	});
 
-	const option = builder(name('option'), {
+	const option = makeElement(name('option'), {
 		stores: [isSelected],
 		returned:
 			([$isSelected]) =>
@@ -590,7 +590,7 @@ export function createListbox<
 		},
 	});
 
-	const group = builder(name('group'), {
+	const group = makeElement(name('group'), {
 		returned: () => {
 			return (groupId: string) => ({
 				role: 'group',
@@ -599,7 +599,7 @@ export function createListbox<
 		},
 	});
 
-	const groupLabel = builder(name('group-label'), {
+	const groupLabel = makeElement(name('group-label'), {
 		returned: () => {
 			return (groupId: string) => ({
 				id: groupId,
@@ -607,20 +607,17 @@ export function createListbox<
 		},
 	});
 
-	const hiddenInput = builder(name('hidden-input'), {
-		stores: [selected, required, nameProp],
-		returned: ([$selected, $required, $name]) => {
+	const hiddenInput = createHiddenInput({
+		value: derived([selected], ([$selected]) => {
 			const value = Array.isArray($selected) ? $selected.map((o) => o.value) : $selected?.value;
-			return {
-				...hiddenInputAttrs,
-				required: $required ? true : undefined,
-				value,
-				name: $name,
-			};
-		},
+			return typeof value === 'string' ? value : JSON.stringify(value);
+		}),
+		name: readonly(nameProp),
+		required,
+		prefix: withDefaults.builder,
 	});
 
-	const arrow = builder(name('arrow'), {
+	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
 		returned: ($arrowSize) => ({
 			'data-arrow': true,
diff --git a/src/lib/builders/menu/create.ts b/src/lib/builders/menu/create.ts
index 2778b6cd..79b6481a 100644
--- a/src/lib/builders/menu/create.ts
+++ b/src/lib/builders/menu/create.ts
@@ -6,7 +6,7 @@ import {
 	addEventListener,
 	addHighlight,
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	createTypeaheadSearch,
 	derivedVisible,
@@ -152,7 +152,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		activeTrigger: rootActiveTrigger,
 	});
 
-	const rootMenu = builder(name(), {
+	const rootMenu = makeElement(name(), {
 		stores: [isVisible, portal, rootIds.menu, rootIds.trigger],
 		returned: ([$isVisible, $portal, $rootMenuId, $rootTriggerId]) => {
 			return {
@@ -266,7 +266,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		},
 	});
 
-	const rootTrigger = builder(name('trigger'), {
+	const rootTrigger = makeElement(name('trigger'), {
 		stores: [rootOpen, rootIds.menu, rootIds.trigger],
 		returned: ([$rootOpen, $rootMenuId, $rootTriggerId]) => {
 			return {
@@ -319,7 +319,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		},
 	});
 
-	const rootArrow = builder(name('arrow'), {
+	const rootArrow = makeElement(name('arrow'), {
 		stores: arrowSize,
 		returned: ($arrowSize) => ({
 			'data-arrow': true,
@@ -331,7 +331,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		}),
 	});
 
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		returned: () => {
 			return {
 				role: 'menuitem',
@@ -395,7 +395,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		},
 	});
 
-	const group = builder(name('group'), {
+	const group = makeElement(name('group'), {
 		returned: () => {
 			return (groupId: string) => ({
 				role: 'group',
@@ -404,7 +404,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		},
 	});
 
-	const groupLabel = builder(name('group-label'), {
+	const groupLabel = makeElement(name('group-label'), {
 		returned: () => {
 			return (groupId: string) => ({
 				id: groupId,
@@ -423,7 +423,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		const checked = overridable(checkedWritable, withDefaults.onCheckedChange);
 		const disabled = writable(withDefaults.disabled);
 
-		const checkboxItem = builder(name('checkbox-item'), {
+		const checkboxItem = makeElement(name('checkbox-item'), {
 			stores: [checked, disabled],
 			returned: ([$checked, $disabled]) => {
 				return {
@@ -530,7 +530,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 		const valueWritable = args.value ?? writable(args.defaultValue ?? null);
 		const value = overridable(valueWritable, args.onValueChange);
 
-		const radioGroup = builder(name('radio-group'), {
+		const radioGroup = makeElement(name('radio-group'), {
 			returned: () => ({
 				role: 'group',
 			}),
@@ -540,7 +540,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			disabled: false,
 		};
 
-		const radioItem = builder(name('radio-item'), {
+		const radioItem = makeElement(name('radio-item'), {
 			stores: [value],
 			returned: ([$value]) => {
 				return (itemProps: _RadioItemProps) => {
@@ -708,7 +708,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			activeTrigger: subActiveTrigger,
 		});
 
-		const subMenu = builder(name('submenu'), {
+		const subMenu = makeElement(name('submenu'), {
 			stores: [subIsVisible, subIds.menu, subIds.trigger],
 			returned: ([$subIsVisible, $subMenuId, $subTriggerId]) => {
 				return {
@@ -848,7 +848,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			},
 		});
 
-		const subTrigger = builder(name('subtrigger'), {
+		const subTrigger = makeElement(name('subtrigger'), {
 			stores: [subOpen, disabled, subIds.menu, subIds.trigger],
 			returned: ([$subOpen, $disabled, $subMenuId, $subTriggerId]) => {
 				return {
@@ -1018,7 +1018,7 @@ export function createMenuBuilder(opts: _MenuBuilderOptions) {
 			},
 		});
 
-		const subArrow = builder(name('subarrow'), {
+		const subArrow = makeElement(name('subarrow'), {
 			stores: arrowSize,
 			returned: ($arrowSize) => ({
 				'data-arrow': true,
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 94f405bd..e3eafda3 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -5,7 +5,7 @@ import {
 	addEventListener,
 	addHighlight,
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	derivedVisible,
 	effect,
@@ -70,7 +70,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 
 	const ids = toWritableStores({ ...generateIds(menubarIdParts), ...withDefaults.ids });
 
-	const menubar = builder(name(), {
+	const menubar = makeElement(name(), {
 		stores: [ids.menubar],
 		returned([$menubarId]) {
 			return {
@@ -135,7 +135,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 			activeTrigger: rootActiveTrigger,
 		});
 
-		const menu = builder(name('menu'), {
+		const menu = makeElement(name('menu'), {
 			stores: [isVisible, portal, m.ids.menu, m.ids.trigger, ids.menubar],
 			returned: ([$isVisible, $portal, $menuId, $triggerId, $menubarId]) => {
 				return {
@@ -246,7 +246,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 			},
 		});
 
-		const trigger = builder(name('trigger'), {
+		const trigger = makeElement(name('trigger'), {
 			stores: [rootOpen, m.ids.menu, m.ids.trigger],
 			returned: ([$rootOpen, $menuId, $triggerId]) => {
 				return {
diff --git a/src/lib/builders/pagination/create.ts b/src/lib/builders/pagination/create.ts
index 3cdf29eb..0fc758b9 100644
--- a/src/lib/builders/pagination/create.ts
+++ b/src/lib/builders/pagination/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	executeCallbacks,
 	isHTMLElement,
@@ -45,7 +45,7 @@ export function createPagination(props: CreatePaginationProps) {
 		return { start, end };
 	});
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		returned: () => ({
 			'data-scope': 'pagination',
 		}),
@@ -91,7 +91,7 @@ export function createPagination(props: CreatePaginationProps) {
 		}
 	};
 
-	const pageTrigger = builder(name('page'), {
+	const pageTrigger = makeElement(name('page'), {
 		stores: page,
 		returned: ($page) => {
 			return (pageItem: Page) => {
@@ -118,7 +118,7 @@ export function createPagination(props: CreatePaginationProps) {
 		},
 	});
 
-	const prevButton = builder(name('prev'), {
+	const prevButton = makeElement(name('prev'), {
 		stores: page,
 		returned: ($page) => {
 			return {
@@ -140,7 +140,7 @@ export function createPagination(props: CreatePaginationProps) {
 		},
 	});
 
-	const nextButton = builder(name('next'), {
+	const nextButton = makeElement(name('next'), {
 		stores: [page, totalPages],
 		returned: ([$page, $totalPages]) => {
 			return {
diff --git a/src/lib/builders/pin-input/create.ts b/src/lib/builders/pin-input/create.ts
index b86f659a..ee780cff 100644
--- a/src/lib/builders/pin-input/create.ts
+++ b/src/lib/builders/pin-input/create.ts
@@ -1,14 +1,13 @@
 import {
 	addMeltEventListener,
-	builder,
 	createElHelpers,
 	disabledAttr,
 	executeCallbacks,
-	hiddenInputAttrs,
 	isBrowser,
 	isHTMLElement,
 	isHTMLInputElement,
 	last,
+	makeElement,
 	next,
 	omit,
 	overridable,
@@ -19,10 +18,12 @@ import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { tick } from 'svelte';
 import { derived, readonly, writable } from 'svelte/store';
 import { generateIds } from '../../internal/helpers/id.js';
+import { createHiddenInput } from '../hidden-input/create.js';
 import type { PinInputEvents } from './events.js';
 import type { CreatePinInputProps } from './types.js';
 
-const { name, selector } = createElHelpers<'input' | 'hidden-input'>('pin-input');
+const prefix = 'pin-input';
+const { name, selector } = createElHelpers<'input' | 'hidden-input'>(prefix);
 
 const getInputs = (node: HTMLInputElement) => {
 	const rootEl = node.closest(selector());
@@ -61,7 +62,7 @@ export function createPinInput(props?: CreatePinInputProps) {
 
 	const ids = toWritableStores({ ...generateIds(pinInputIdParts), ...withDefaults.ids });
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [value, ids.root],
 		returned: ([$value, $rootId]) => {
 			return {
@@ -82,7 +83,7 @@ export function createPinInput(props?: CreatePinInputProps) {
 		return inputs.length;
 	};
 
-	const input = builder(name('input'), {
+	const input = makeElement(name('input'), {
 		stores: [value, placeholder, disabled, type],
 		returned: ([$value, $placeholder, $disabled, $type]) => {
 			return () => {
@@ -231,13 +232,11 @@ export function createPinInput(props?: CreatePinInputProps) {
 		},
 	});
 
-	const hiddenInput = builder(name('hidden-input'), {
-		stores: [valueStr, nameStore],
-		returned: ([$valueStr, $nameStore]) => ({
-			...hiddenInputAttrs,
-			value: $valueStr,
-			name: $nameStore,
-		}),
+	const hiddenInput = createHiddenInput({
+		value: valueStr,
+		disabled,
+		name: nameStore,
+		prefix,
 	});
 
 	const clear = () => {
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index 9185b6b8..8fa0b2f5 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	derivedVisible,
 	effect,
@@ -89,7 +89,7 @@ export function createPopover(args?: CreatePopoverProps) {
 
 	const isVisible = derivedVisible({ open, activeTrigger, forceVisible });
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [isVisible, portal, ids.content],
 		returned: ([$isVisible, $portal, $contentId]) => {
 			return {
@@ -192,7 +192,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		handleClose();
 	}
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [open, ids.content, ids.trigger],
 		returned: ([$open, $contentId, $triggerId]) => {
 			return {
@@ -222,7 +222,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		},
 	});
 
-	const arrow = builder(name('arrow'), {
+	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
 		returned: ($arrowSize) => ({
 			'data-arrow': true,
@@ -234,7 +234,7 @@ export function createPopover(args?: CreatePopoverProps) {
 		}),
 	});
 
-	const close = builder(name('close'), {
+	const close = makeElement(name('close'), {
 		returned: () =>
 			({
 				type: 'button',
diff --git a/src/lib/builders/progress/create.ts b/src/lib/builders/progress/create.ts
index 7d97ad85..5c3f725f 100644
--- a/src/lib/builders/progress/create.ts
+++ b/src/lib/builders/progress/create.ts
@@ -1,5 +1,5 @@
 import {
-	builder,
+	makeElement,
 	createElHelpers,
 	omit,
 	overridable,
@@ -24,7 +24,7 @@ export const createProgress = (props?: CreateProgressProps) => {
 	const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue);
 	const value = overridable(valueWritable, withDefaults?.onValueChange);
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [value, max],
 		returned: ([$value, $max]) => {
 			return {
diff --git a/src/lib/builders/radio-group/create.ts b/src/lib/builders/radio-group/create.ts
index edac3f04..331a5fdf 100644
--- a/src/lib/builders/radio-group/create.ts
+++ b/src/lib/builders/radio-group/create.ts
@@ -1,16 +1,15 @@
 import {
 	addEventListener,
 	addMeltEventListener,
-	builder,
 	createElHelpers,
 	disabledAttr,
 	effect,
 	executeCallbacks,
 	getDirectionalKeys,
 	getElemDirection,
-	hiddenInputAttrs,
 	isHTMLElement,
 	kbd,
+	makeElement,
 	omit,
 	overridable,
 	toWritableStores,
@@ -18,6 +17,7 @@ import {
 import { safeOnMount } from '$lib/internal/helpers/lifecycle.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
 import { derived, writable } from 'svelte/store';
+import { createHiddenInput } from '../hidden-input/create.js';
 import type { RadioGroupEvents } from './events.js';
 import type { CreateRadioGroupProps, RadioGroupItemProps } from './types.js';
 
@@ -30,7 +30,8 @@ const defaults = {
 } satisfies Defaults<CreateRadioGroupProps>;
 
 type RadioGroupParts = 'item' | 'hidden-input';
-const { name, selector } = createElHelpers<RadioGroupParts>('radio-group');
+const prefix = 'radio-group';
+const { name, selector } = createElHelpers<RadioGroupParts>(prefix);
 
 export function createRadioGroup(props?: CreateRadioGroupProps) {
 	const withDefaults = { ...defaults, ...props } satisfies CreateRadioGroupProps;
@@ -78,7 +79,7 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 	};
 
 	/** Elements */
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [required, orientation],
 		returned: ([$required, $orientation]) => {
 			return {
@@ -89,7 +90,7 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 		},
 	});
 
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		stores: [value, orientation, disabled],
 		returned: ([$value, $orientation, $disabled]) => {
 			return (props: RadioGroupItemProps) => {
@@ -174,20 +175,10 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 		},
 	});
 
-	const hiddenInput = builder(name('hidden-input'), {
-		stores: [disabled, value, required],
-		returned: ([$disabled, $value, $required]) => {
-			return {
-				...hiddenInputAttrs,
-				disabled: disabledAttr($disabled),
-				value: $value,
-				required: $required,
-			};
-		},
-		action: (_node: HTMLInputElement) => {
-			_node;
-			// no-op, just to enforce the type of the element
-		},
+	const hiddenInput = createHiddenInput({
+		value,
+		disabled,
+		required,
 	});
 
 	const isChecked = derived(value, ($value) => {
diff --git a/src/lib/builders/range-calendar/create.ts b/src/lib/builders/range-calendar/create.ts
index bd011ccc..8702c21d 100644
--- a/src/lib/builders/range-calendar/create.ts
+++ b/src/lib/builders/range-calendar/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -275,7 +275,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 		}
 	);
 
-	const calendar = builder(name(), {
+	const calendar = makeElement(name(), {
 		stores: [fullCalendarLabel, isInvalid, ids.calendar, disabled, readonly],
 		returned: ([$fullCalendarLabel, $isInvalid, $calendarId, $disabled, $readonly]) => {
 			return {
@@ -306,7 +306,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 		},
 	});
 
-	const heading = builder(name('heading'), {
+	const heading = makeElement(name('heading'), {
 		stores: [disabled],
 		returned: ([$disabled]) => {
 			return {
@@ -316,7 +316,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 		},
 	});
 
-	const grid = builder(name('grid'), {
+	const grid = makeElement(name('grid'), {
 		stores: [readonly, disabled],
 		returned: ([$readonly, $disabled]) => ({
 			tabindex: -1,
@@ -328,7 +328,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 		}),
 	});
 
-	const prevButton = builder(name('prevButton'), {
+	const prevButton = makeElement(name('prevButton'), {
 		stores: [isPrevButtonDisabled],
 		returned: ([$isPrevButtonDisabled]) => {
 			const disabled = $isPrevButtonDisabled;
@@ -352,7 +352,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 		},
 	});
 
-	const nextButton = builder(name('nextButton'), {
+	const nextButton = makeElement(name('nextButton'), {
 		stores: [isNextButtonDisabled],
 		returned: ([$isNextButtonDisabled]) => {
 			const disabled = $isNextButtonDisabled;
@@ -432,7 +432,7 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 	 * An individual date cell in the calendar grid, which represents a
 	 * single day in the month.
 	 */
-	const cell = builder(name('cell'), {
+	const cell = makeElement(name('cell'), {
 		stores: [
 			isSelected,
 			isSelectionEnd,
diff --git a/src/lib/builders/separator/create.ts b/src/lib/builders/separator/create.ts
index 29a1b79d..d4d2aeab 100644
--- a/src/lib/builders/separator/create.ts
+++ b/src/lib/builders/separator/create.ts
@@ -1,4 +1,4 @@
-import { builder, toWritableStores } from '$lib/internal/helpers/index.js';
+import { makeElement, toWritableStores } from '$lib/internal/helpers/index.js';
 import type { Defaults } from '$lib/internal/types.js';
 import type { CreateSeparatorProps } from './types.js';
 
@@ -12,7 +12,7 @@ export const createSeparator = (props?: CreateSeparatorProps) => {
 	const options = toWritableStores(withDefaults);
 	const { orientation, decorative } = options;
 
-	const root = builder('separator', {
+	const root = makeElement('separator', {
 		stores: [orientation, decorative],
 		returned: ([$orientation, $decorative]) => {
 			const ariaOrientation = $orientation === 'vertical' ? $orientation : undefined;
diff --git a/src/lib/builders/slider/create.ts b/src/lib/builders/slider/create.ts
index 8f961360..b2fa5e22 100644
--- a/src/lib/builders/slider/create.ts
+++ b/src/lib/builders/slider/create.ts
@@ -1,8 +1,8 @@
 import {
 	addEventListener,
 	addMeltEventListener,
-	builder,
-	builderArray,
+	makeElement,
+	makeElementArray,
 	createElHelpers,
 	disabledAttr,
 	effect,
@@ -112,7 +112,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 	});
 
 	// Elements
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [disabled, orientation, dir],
 		returned: ([$disabled, $orientation, $dir]) => {
 			return {
@@ -129,7 +129,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 		},
 	});
 
-	const range = builder(name('range'), {
+	const range = makeElement(name('range'), {
 		stores: [value, direction, position],
 		returned: ([$value, $direction, $position]) => {
 			const minimum = $value.length > 1 ? $position(Math.min(...$value) ?? 0) : 0;
@@ -168,7 +168,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 		},
 	});
 
-	const thumbs = builderArray(name('thumb'), {
+	const thumbs = makeElementArray(name('thumb'), {
 		stores: [value, position, min, max, disabled, orientation, direction],
 		returned: ([$value, $position, $min, $max, $disabled, $orientation, $direction]) => {
 			const result = Array.from({ length: $value.length || 1 }, (_, i) => {
@@ -326,7 +326,7 @@ export const createSlider = (props?: CreateSliderProps) => {
 		},
 	});
 
-	const ticks = builderArray(name('tick'), {
+	const ticks = makeElementArray(name('tick'), {
 		stores: [value, min, max, step, direction],
 		returned: ([$value, $min, $max, $step, $direction]) => {
 			const difference = $max - $min;
diff --git a/src/lib/builders/switch/create.ts b/src/lib/builders/switch/create.ts
index 282b4b2a..4d1aa06e 100644
--- a/src/lib/builders/switch/create.ts
+++ b/src/lib/builders/switch/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	kbd,
@@ -39,7 +39,7 @@ export function createSwitch(props?: CreateSwitchProps) {
 		checked.update((prev) => !prev);
 	}
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: [checked, disabled, required],
 		returned: ([$checked, $disabled, $required]) => {
 			return {
@@ -70,7 +70,7 @@ export function createSwitch(props?: CreateSwitchProps) {
 		},
 	});
 
-	const input = builder(name('input'), {
+	const input = makeElement(name('input'), {
 		stores: [checked, nameStore, required, disabled, value],
 		returned: ([$checked, $name, $required, $disabled, $value]) => {
 			return {
diff --git a/src/lib/builders/table-of-contents/create.ts b/src/lib/builders/table-of-contents/create.ts
index e3195bfb..043d3d13 100644
--- a/src/lib/builders/table-of-contents/create.ts
+++ b/src/lib/builders/table-of-contents/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	executeCallbacks,
 } from '$lib/internal/helpers/index.js';
@@ -345,7 +345,7 @@ export function createTableOfContents(args: CreateTableOfContentsArgs) {
 	});
 
 	// Elements
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		stores: activeHeadingIdxs,
 		returned: ($activeHeadingIdxs) => {
 			return (id: string) => {
diff --git a/src/lib/builders/tabs/create.ts b/src/lib/builders/tabs/create.ts
index ccca4e9d..8dac2383 100644
--- a/src/lib/builders/tabs/create.ts
+++ b/src/lib/builders/tabs/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	executeCallbacks,
@@ -45,7 +45,7 @@ export function createTabs(props?: CreateTabsProps) {
 	let ssrValue = withDefaults.defaultValue ?? value.get();
 
 	// Root
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: orientation,
 		returned: ($orientation) => {
 			return {
@@ -55,7 +55,7 @@ export function createTabs(props?: CreateTabsProps) {
 	});
 
 	// List
-	const list = builder(name('list'), {
+	const list = makeElement(name('list'), {
 		stores: orientation,
 		returned: ($orientation) => {
 			return {
@@ -74,7 +74,7 @@ export function createTabs(props?: CreateTabsProps) {
 		}
 	};
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [value, orientation],
 		returned: ([$value, $orientation]) => {
 			return (props: TabsTriggerProps) => {
@@ -178,7 +178,7 @@ export function createTabs(props?: CreateTabsProps) {
 	});
 
 	// Content
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: value,
 		returned: ($value) => {
 			return (tabValue: string) => {
diff --git a/src/lib/builders/tags-input/create.ts b/src/lib/builders/tags-input/create.ts
index 83bec898..12c1c6b3 100644
--- a/src/lib/builders/tags-input/create.ts
+++ b/src/lib/builders/tags-input/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	effect,
@@ -226,7 +226,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		return true;
 	}
 
-	const root = builder(name(''), {
+	const root = makeElement(name(''), {
 		stores: [disabled],
 		returned: ([$disabled]) => {
 			return {
@@ -254,7 +254,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		},
 	});
 
-	const input = builder(name('input'), {
+	const input = makeElement(name('input'), {
 		stores: [disabled, placeholder],
 		returned: ([$disabled, $placeholder]) => {
 			return {
@@ -468,7 +468,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		},
 	});
 
-	const tag = builder(name('tag'), {
+	const tag = makeElement(name('tag'), {
 		stores: [selected, editing, disabled, editable],
 		returned: ([$selected, $editing, $disabled, $editable]) => {
 			return (tag: TagProps) => {
@@ -565,7 +565,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		},
 	});
 
-	const deleteTrigger = builder(name('delete-trigger'), {
+	const deleteTrigger = makeElement(name('delete-trigger'), {
 		stores: [selected, editing, disabled, editable],
 		returned: ([$selected, $editing, $disabled, $editable]) => {
 			return (tag: TagProps) => {
@@ -616,7 +616,7 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 		},
 	});
 
-	const edit = builder(name('edit'), {
+	const edit = makeElement(name('edit'), {
 		stores: [editing, editable],
 		returned: ([$editing, $editable]) => {
 			return (tag: Tag) => {
diff --git a/src/lib/builders/toast/create.ts b/src/lib/builders/toast/create.ts
index ab3b4719..cf3b30a0 100644
--- a/src/lib/builders/toast/create.ts
+++ b/src/lib/builders/toast/create.ts
@@ -1,7 +1,7 @@
 import { usePortal } from '$lib/internal/actions/index.js';
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	executeCallbacks,
 	generateId,
@@ -98,7 +98,7 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		});
 	};
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: toastsMap,
 		returned: ($toasts) => {
 			return (id: string) => {
@@ -162,7 +162,7 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		},
 	});
 
-	const title = builder(name('title'), {
+	const title = makeElement(name('title'), {
 		stores: toastsMap,
 		returned: ($toasts) => {
 			return (id: string) => {
@@ -175,7 +175,7 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		},
 	});
 
-	const description = builder(name('description'), {
+	const description = makeElement(name('description'), {
 		stores: toastsMap,
 		returned: ($toasts) => {
 			return (id: string) => {
@@ -189,7 +189,7 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		},
 	});
 
-	const close = builder(name('close'), {
+	const close = makeElement(name('close'), {
 		returned: () => {
 			return (id: string) => ({
 				type: 'button',
diff --git a/src/lib/builders/toggle-group/create.ts b/src/lib/builders/toggle-group/create.ts
index 5ca1ede7..f79af420 100644
--- a/src/lib/builders/toggle-group/create.ts
+++ b/src/lib/builders/toggle-group/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	executeCallbacks,
@@ -47,7 +47,7 @@ export const createToggleGroup = <T extends ToggleGroupType = 'single'>(
 	const valueWritable = withDefaults.value ?? writable(defaultValue);
 	const value = overridable(valueWritable, withDefaults?.onValueChange);
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: orientation,
 		returned: ($orientation) => {
 			return {
@@ -57,7 +57,7 @@ export const createToggleGroup = <T extends ToggleGroupType = 'single'>(
 		},
 	});
 
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		stores: [value, disabled, orientation, type],
 		returned: ([$value, $disabled, $orientation, $type]) => {
 			return (props: ToggleGroupItemProps) => {
diff --git a/src/lib/builders/toggle/create.ts b/src/lib/builders/toggle/create.ts
index 03b1d19c..09a1c409 100644
--- a/src/lib/builders/toggle/create.ts
+++ b/src/lib/builders/toggle/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	disabledAttr,
 	executeCallbacks,
 	kbd,
@@ -33,7 +33,7 @@ export function createToggle(props?: CreateToggleProps) {
 		pressed.update((v) => !v);
 	}
 
-	const root = builder('toggle', {
+	const root = makeElement('toggle', {
 		stores: [pressed, disabled],
 		returned: ([$pressed, $disabled]) => {
 			return {
diff --git a/src/lib/builders/toggle/types.ts b/src/lib/builders/toggle/types.ts
index 54ea091d..db0775c8 100644
--- a/src/lib/builders/toggle/types.ts
+++ b/src/lib/builders/toggle/types.ts
@@ -3,6 +3,7 @@ import type { Writable } from 'svelte/store';
 import type { createToggle } from './create.js';
 import type { ChangeFn } from '$lib/internal/helpers/index.js';
 export type { ToggleComponentEvents } from './events.js';
+
 export type CreateToggleProps = {
 	disabled?: boolean;
 	defaultPressed?: boolean;
diff --git a/src/lib/builders/toolbar/create.ts b/src/lib/builders/toolbar/create.ts
index 2cc74d95..11641e0f 100644
--- a/src/lib/builders/toolbar/create.ts
+++ b/src/lib/builders/toolbar/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	disabledAttr,
 	executeCallbacks,
@@ -33,7 +33,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 	const options = toWritableStores(withDefaults);
 	const { loop, orientation } = options;
 
-	const root = builder(name(), {
+	const root = makeElement(name(), {
 		stores: orientation,
 		returned: ($orientation) => {
 			return {
@@ -43,7 +43,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		},
 	});
 
-	const button = builder(name('button'), {
+	const button = makeElement(name('button'), {
 		returned: () =>
 			({
 				role: 'button',
@@ -59,7 +59,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		},
 	});
 
-	const link = builder(name('link'), {
+	const link = makeElement(name('link'), {
 		returned: () =>
 			({
 				role: 'link',
@@ -75,7 +75,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 		},
 	});
 
-	const separator = builder(name('separator'), {
+	const separator = makeElement(name('separator'), {
 		stores: orientation,
 		returned: ($orientation) => {
 			return {
@@ -111,7 +111,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 
 		const { name } = createElHelpers('toolbar-group');
 
-		const group = builder(name(), {
+		const group = makeElement(name(), {
 			stores: orientation,
 			returned: ($orientation) => {
 				return {
@@ -121,7 +121,7 @@ export const createToolbar = (props?: CreateToolbarProps) => {
 			},
 		});
 
-		const item = builder(name('item'), {
+		const item = makeElement(name('item'), {
 			stores: [disabled, type, value, orientation],
 			returned: ([$disabled, $type, $value, $orientation]) => {
 				return (props: ToolbarGroupItemProps) => {
diff --git a/src/lib/builders/tooltip/create.ts b/src/lib/builders/tooltip/create.ts
index 6ef76d8b..a4383ddb 100644
--- a/src/lib/builders/tooltip/create.ts
+++ b/src/lib/builders/tooltip/create.ts
@@ -1,7 +1,7 @@
 import {
 	addEventListener,
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	effect,
 	executeCallbacks,
@@ -132,7 +132,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 		return $open || $forceVisible;
 	});
 
-	const trigger = builder(name('trigger'), {
+	const trigger = makeElement(name('trigger'), {
 		stores: [ids.content, ids.trigger, open],
 		returned: ([$contentId, $triggerId, $open]) => {
 			return {
@@ -190,7 +190,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 		},
 	});
 
-	const content = builder(name('content'), {
+	const content = makeElement(name('content'), {
 		stores: [isVisible, open, portal, ids.content],
 		returned: ([$isVisible, $open, $portal, $contentId]) => {
 			return {
@@ -266,7 +266,7 @@ export function createTooltip(props?: CreateTooltipProps) {
 		},
 	});
 
-	const arrow = builder(name('arrow'), {
+	const arrow = makeElement(name('arrow'), {
 		stores: arrowSize,
 		returned: ($arrowSize) => ({
 			'data-arrow': true,
diff --git a/src/lib/builders/tree/create.ts b/src/lib/builders/tree/create.ts
index d04ce284..d7bacbb2 100644
--- a/src/lib/builders/tree/create.ts
+++ b/src/lib/builders/tree/create.ts
@@ -1,6 +1,6 @@
 import {
 	addMeltEventListener,
-	builder,
+	makeElement,
 	createElHelpers,
 	executeCallbacks,
 	getElementByMeltId,
@@ -69,7 +69,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 
 	const metaIds = generateIds(['tree']);
 
-	const rootTree = builder(name(), {
+	const rootTree = makeElement(name(), {
 		returned: () => {
 			return {
 				role: 'tree',
@@ -79,7 +79,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 	});
 
 	let isKeydown = false;
-	const item = builder(name('item'), {
+	const item = makeElement(name('item'), {
 		stores: [expanded, selectedId, lastFocusedId],
 		returned: ([$expanded, $selectedId, $lastFocusedId]) => {
 			return (opts: { id: string; hasChildren?: boolean }) => {
@@ -249,7 +249,7 @@ export function createTreeView(args?: CreateTreeViewProps) {
 		},
 	});
 
-	const group = builder(name('group'), {
+	const group = makeElement(name('group'), {
 		stores: [expanded],
 		returned: ([$expanded]) => {
 			return (opts: { id: string }) => ({
diff --git a/src/lib/internal/helpers/index.ts b/src/lib/internal/helpers/index.ts
index 74202cd2..ad3fb301 100644
--- a/src/lib/internal/helpers/index.ts
+++ b/src/lib/internal/helpers/index.ts
@@ -1,6 +1,6 @@
 export * from './array.js';
 export * from './attr.js';
-export * from './builder.js';
+export * from './makeElement.js';
 export * from './dom.js';
 export * from './event.js';
 export * from './highlight.js';
diff --git a/src/lib/internal/helpers/builder.ts b/src/lib/internal/helpers/makeElement.ts
similarity index 87%
rename from src/lib/internal/helpers/builder.ts
rename to src/lib/internal/helpers/makeElement.ts
index c1539ae5..7d0df75a 100644
--- a/src/lib/internal/helpers/builder.ts
+++ b/src/lib/internal/helpers/makeElement.ts
@@ -20,7 +20,7 @@ export const hiddenAction = <T extends Record<string, unknown>>(obj: T) => {
 	});
 };
 
-type BuilderCallback<S extends Stores | undefined> = S extends Stores
+type ElementCallback<S extends Stores | undefined> = S extends Stores
 	? // eslint-disable-next-line @typescript-eslint/no-explicit-any
 	  (values: StoresValues<S>) => Record<string, any> | ((...args: any[]) => Record<string, any>)
 	: // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -32,22 +32,22 @@ const isFunctionWithParams = (
 	return typeof fn === 'function';
 };
 
-type BuilderArgs<
+type MakeElementArgs<
 	S extends Stores | undefined,
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
-	R extends BuilderCallback<S>
+	R extends ElementCallback<S>
 > = {
 	stores?: S;
 	action?: A;
 	returned?: R;
 };
 
-type BuilderStore<
+type ElementStore<
 	S extends Stores | undefined,
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
-	R extends BuilderCallback<S>,
+	R extends ElementCallback<S>,
 	Name extends string
 > = Readable<
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -58,13 +58,13 @@ type BuilderStore<
 		: ReturnType<R> & { [K in `data-melt-${Name}`]: '' } & { action: A }
 >;
 
-export function builder<
+export function makeElement<
 	S extends Stores | undefined,
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
-	R extends BuilderCallback<S>,
+	R extends ElementCallback<S>,
 	Name extends string
->(name: Name, args?: BuilderArgs<S, A, R>): ExplicitBuilderReturn<S, A, R, Name> {
+>(name: Name, args?: MakeElementArgs<S, A, R>): ExplicitMakeElementReturn<S, A, R, Name> {
 	const { stores, action, returned } = args ?? {};
 
 	const derivedStore = (() => {
@@ -116,7 +116,7 @@ export function builder<
 				})
 			);
 		}
-	})() as BuilderStore<S, A, R, Name>;
+	})() as ElementStore<S, A, R, Name>;
 
 	const actionFn = (action ??
 		(() => {
@@ -127,13 +127,13 @@ export function builder<
 	return actionFn;
 }
 
-export type ExplicitBuilderReturn<
+export type ExplicitMakeElementReturn<
 	S extends Stores | undefined,
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
-	R extends BuilderCallback<S>,
+	R extends ElementCallback<S>,
 	Name extends string
-> = BuilderStore<S, A, R, Name> & A;
+> = ElementStore<S, A, R, Name> & A;
 
 type BuilderArrayArgs<
 	S extends Stores,
@@ -146,13 +146,13 @@ type BuilderArrayArgs<
 	action?: A;
 };
 
-export function builderArray<
+export function makeElementArray<
 	S extends Stores,
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
 	R extends object[],
 	Name extends string
->(name: Name, args: BuilderArrayArgs<S, A, R>): ExplicitBuilderArrayReturn<A, R, Name> {
+>(name: Name, args: BuilderArrayArgs<S, A, R>): ExplicitMakeElementArrayReturn<A, R, Name> {
 	const { stores, returned, action } = args;
 
 	const { subscribe } = derived(stores, (values) =>
@@ -163,7 +163,7 @@ export function builderArray<
 				action: action ?? noop,
 			})
 		)
-	) as BuilderArrayStore<A, R, Name>;
+	) as ElementArrayStore<A, R, Name>;
 
 	const actionFn = (action ??
 		(() => {
@@ -174,7 +174,7 @@ export function builderArray<
 	return actionFn;
 }
 
-type BuilderArrayStore<
+type ElementArrayStore<
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
 	R extends object[],
@@ -183,12 +183,12 @@ type BuilderArrayStore<
 	[K in keyof R]: R[K] & { [K in `data-melt-${Name}`]: '' } & { action: A };
 }>;
 
-export type ExplicitBuilderArrayReturn<
+export type ExplicitMakeElementArrayReturn<
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	A extends Action<any, any>,
 	R extends object[],
 	Name extends string
-> = BuilderArrayStore<A, R, Name> & A;
+> = ElementArrayStore<A, R, Name> & A;
 
 export function createElHelpers<Part extends string = string>(prefix: string) {
 	const name = (part?: Part) => (part ? `${prefix}-${part}` : prefix);
diff --git a/src/lib/internal/helpers/object.ts b/src/lib/internal/helpers/object.ts
index 26d49f66..49add7e2 100644
--- a/src/lib/internal/helpers/object.ts
+++ b/src/lib/internal/helpers/object.ts
@@ -47,3 +47,18 @@ export function stripValues<T extends Record<string, unknown>, ToStrip>(
 		Object.entries(inputObject).filter(([_, value]) => !dequal(value, toStrip))
 	) as typeof recursive extends true ? StripValuesRecursive<T, ToStrip> : StripValues<T, ToStrip>;
 }
+
+export function removeUndefined<T extends object>(
+	obj: T
+): {
+	[K in keyof T]-?: Exclude<T[K], undefined>;
+} {
+	const result = {} as { [K in keyof T]-?: Exclude<T[K], undefined> };
+	for (const key in obj) {
+		const value = obj[key];
+		if (value !== undefined) {
+			result[key] = value as any;
+		}
+	}
+	return result;
+}
diff --git a/src/lib/internal/helpers/store/toReadableStores.ts b/src/lib/internal/helpers/store/toReadableStores.ts
new file mode 100644
index 00000000..6e142847
--- /dev/null
+++ b/src/lib/internal/helpers/store/toReadableStores.ts
@@ -0,0 +1,33 @@
+import { readable, type Readable } from 'svelte/store';
+import { isReadable } from '../is.js';
+import { withGet, type WithGet } from '../withGet.js';
+
+type TODO = any;
+
+export type ToReadableStores<T extends Record<string, unknown>> = {
+	[K in keyof T]: T[K] extends Readable<TODO> ? WithGet<T[K]> : WithGet<Readable<T[K]>>;
+};
+
+/**
+ * Given an object of properties, returns an object of writable stores
+ * with the same properties and values.
+ */
+export function toReadableStores<T extends Record<string, unknown>>(
+	properties: T
+): ToReadableStores<T> {
+	const result = {} as {
+		[K in keyof T]: T[K] extends Readable<TODO> ? WithGet<T[K]> : WithGet<Readable<T[K]>>;
+	};
+
+	Object.keys(properties).forEach((key) => {
+		const propertyKey = key as keyof T;
+		const value = properties[propertyKey];
+		if (isReadable(value)) {
+			result[propertyKey] = withGet(value) as TODO;
+		} else {
+			result[propertyKey] = withGet(readable(value)) as TODO;
+		}
+	});
+
+	return result;
+}


Test patch:
diff --git a/src/tests/hidden-input/HiddenInput.spec.ts b/src/tests/hidden-input/HiddenInput.spec.ts
new file mode 100644
index 00000000..41cd07a1
--- /dev/null
+++ b/src/tests/hidden-input/HiddenInput.spec.ts
@@ -0,0 +1,62 @@
+import { render } from '@testing-library/svelte';
+import { userEvent } from '@testing-library/user-event';
+import { writable } from 'svelte/store';
+import { test } from 'vitest';
+import type { HiddenInputProps } from './HiddenInputTest.svelte';
+import HiddenInputTest from './HiddenInputTest.svelte';
+import { sleep } from '$lib/internal/helpers/sleep.js';
+
+/**
+ * Simple setup function to render the tooltip component and
+ * return the user event object, trigger, content, and other
+ * methods returned from the render function.
+ */
+function setup(props: HiddenInputProps) {
+	const user = userEvent.setup();
+	const returned = render(HiddenInputTest, { props });
+	const input = returned.getByTestId('input') as HTMLInputElement;
+
+	return {
+		...returned,
+		user,
+		input,
+	};
+}
+
+describe('Hidden Input', () => {
+	test('It has the correct attributes', async () => {
+		const value = writable('test');
+		const name = writable('test');
+		const disabled = writable(false);
+		const required = writable(false);
+		const { input } = setup({ value, name, disabled, required });
+
+		expect(input.value).toBe('test');
+		value.set('new value');
+		expect(input.value).toBe('new value');
+
+		expect(input.name).toBe('test');
+		name.set('new name');
+		await sleep(1);
+		expect(input.name).toBe('new name');
+
+		expect(input.disabled).toBe(false);
+		disabled.set(true);
+		await sleep(1);
+		expect(input.disabled).toBe(true);
+
+		expect(input.required).toBe(false);
+		required.set(true);
+		await sleep(1);
+		expect(input.required).toBe(true);
+	});
+
+	test('When value is changed, an event is emitted', async () => {
+		const value = writable('test');
+		let called = 0;
+		setup({ value, onChange: () => called++ });
+
+		value.set('new value');
+		expect(called).toBe(1);
+	});
+});
diff --git a/src/tests/hidden-input/HiddenInputTest.svelte b/src/tests/hidden-input/HiddenInputTest.svelte
new file mode 100644
index 00000000..b006aafc
--- /dev/null
+++ b/src/tests/hidden-input/HiddenInputTest.svelte
@@ -0,0 +1,30 @@
+<script lang="ts" context="module">
+	export type HiddenInputProps = CreateHiddenInputProps & {
+		onChange?: (event: Event) => void;
+	};
+</script>
+
+<script lang="ts">
+	import { melt } from '$lib/index.js';
+	import {
+		createHiddenInput,
+		type CreateHiddenInputProps,
+	} from '$lib/builders/hidden-input/index.js';
+	import { noop } from '$lib/internal/helpers/callbacks.js';
+
+	type $$Props = HiddenInputProps;
+	export let name: $$Props['name'] = undefined;
+	export let value: $$Props['value'];
+	export let disabled: $$Props['disabled'] = undefined;
+	export let required: $$Props['required'] = undefined;
+	export let onChange: $$Props['onChange'] = noop;
+
+	const hiddenInput = createHiddenInput({
+		name,
+		value,
+		disabled,
+		required,
+	});
+</script>
+
+<input use:melt={$hiddenInput} on:change={onChange} data-testid="input" />
diff --git a/src/tests/utils.ts b/src/tests/utils.ts
index 3a9f747c..69fd23c6 100644
--- a/src/tests/utils.ts
+++ b/src/tests/utils.ts
@@ -1,5 +1,5 @@
-import { kbd } from '$lib/internal/helpers/index.js';
-
+import { kbd, removeUndefined } from '$lib/internal/helpers/index.js';
+export { removeUndefined };
 type KbdKeys = keyof typeof kbd;
 /**
  * A wrapper around the internal kbd object to make it easier to use
@@ -10,17 +10,6 @@ export const testKbd: Record<KbdKeys, string> = Object.entries(kbd).reduce((acc,
 	return acc;
 }, {} as Record<KbdKeys, string>);
 
-export function removeUndefined<T extends object>(obj: T): T {
-	const result = {} as T;
-	for (const key in obj) {
-		const value = obj[key];
-		if (value !== undefined) {
-			result[key] = value;
-		}
-	}
-	return result;
-}
-
 export function exists(get: (id: string) => HTMLElement, testId: string) {
 	try {
 		get(testId);