diff --git a/.changeset/sweet-brooms-wonder.md b/.changeset/sweet-brooms-wonder.md
new file mode 100644
index 000000000000..557ce89e9ed0
--- /dev/null
+++ b/.changeset/sweet-brooms-wonder.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: allow runes in POJO properties
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 2eac934b332c..2aba9f3c41c8 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -118,7 +118,8 @@ export function CallExpression(node, context) {
 			if (
 				(parent.type !== 'VariableDeclarator' ||
 					get_parent(context.path, -3).type === 'ConstTag') &&
-				!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
+				!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) &&
+				!(parent.type === 'Property' && parent.value === node)
 			) {
 				e.state_invalid_placement(node, rune);
 			}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index 0bdfbae746d0..8c60596e99af 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -36,6 +36,7 @@ import { KeyBlock } from './visitors/KeyBlock.js';
 import { LabeledStatement } from './visitors/LabeledStatement.js';
 import { LetDirective } from './visitors/LetDirective.js';
 import { MemberExpression } from './visitors/MemberExpression.js';
+import { ObjectExpression } from './visitors/ObjectExpression.js';
 import { OnDirective } from './visitors/OnDirective.js';
 import { Program } from './visitors/Program.js';
 import { RegularElement } from './visitors/RegularElement.js';
@@ -111,6 +112,7 @@ const visitors = {
 	LabeledStatement,
 	LetDirective,
 	MemberExpression,
+	ObjectExpression,
 	OnDirective,
 	Program,
 	RegularElement,
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js
new file mode 100644
index 000000000000..d2d798d8a816
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js
@@ -0,0 +1,73 @@
+/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement, Statement } from 'estree' */
+/** @import { Context } from '../types' */
+import * as b from '../../../../utils/builders.js';
+import { get_rune } from '../../../scope.js';
+import { should_proxy } from '../utils.js';
+
+/**
+ * @param {ObjectExpression} node
+ * @param {Context} context
+ */
+export function ObjectExpression(node, context) {
+	let has_runes = false;
+	const valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by'];
+	/** @type {Statement[]} */
+	const body = [];
+	let counter = 0;
+	/** @type {(Property | SpreadElement)[]} */
+	const properties = [];
+	for (let property of node.properties) {
+		if (property.type !== 'Property') {
+			properties.push(/** @type {SpreadElement} */ (context.visit(property)));
+			continue;
+		}
+		const rune = get_rune(property.value, context.state.scope);
+		if (rune && valid_property_runes.includes(rune)) {
+			has_runes = true;
+			const name = context.state.scope.generate(`$$${++counter}`);
+			const call = rune.match(/^\$state/) ? '$.state' : '$.derived';
+			/** @type {Expression} */
+			let value = /** @type {Expression} */ (
+				context.visit(/** @type {CallExpression} */ (property.value).arguments[0] ?? b.void0)
+			);
+			const key = /** @type {Expression} */ (context.visit(property.key));
+			value =
+				rune === '$derived'
+					? b.thunk(value)
+					: rune === '$state' && should_proxy(value, context.state.scope)
+						? b.call('$.proxy', value)
+						: value;
+			properties.push(
+				b.prop(
+					'get',
+					key,
+					b.function(null, [], b.block([b.return(b.call('$.get', b.id(name)))])),
+					property.computed
+				),
+				b.prop(
+					'set',
+					key,
+					b.function(
+						null,
+						[b.id('$$value')],
+						b.block([
+							b.stmt(
+								b.call('$.set', b.id(name), b.id('$$value'), rune === '$state' ? b.true : undefined)
+							)
+						])
+					),
+					property.computed
+				)
+			);
+			body.push(b.let(name, b.call(call, value)));
+		} else {
+			properties.push(/** @type {Property} */ (context.visit(property)));
+		}
+	}
+	if (!has_runes) {
+		context.next();
+		return;
+	}
+	body.push(b.return(b.object(properties)));
+	return b.call(b.arrow([], b.block(body)));
+}