diff --git a/.changeset/27398-currency-empty-placeholder.md b/.changeset/27398-currency-empty-placeholder.md
new file mode 100644
index 00000000000..62f7a68c25a
--- /dev/null
+++ b/.changeset/27398-currency-empty-placeholder.md
@@ -0,0 +1,12 @@
+---
+'@object-ui/fields': patch
+---
+
+fix(fields): an empty currency or percent box shows the field's own placeholder, or none, instead of `0.00` / `0` (#27398)
+
+Clause-②: no
+Metadata-protocol: none
+
+The edit-mode `CurrencyField` fell back to the placeholder `0.00` whenever the field declared none, so an optional amount nobody had filled in (a public form's Annual Revenue) read as a filled-in zero, and a JPY or KWD field was offered a two-decimal stand-in. It now shows the field's `placeholder` when one is declared and nothing otherwise, as `NumberField` does. It reaches every host that draws the widget: the public form, the record form and the grid's inline editor. A real zero still shows as `0`, and an authored placeholder (including one a form row overrides) is shown as before.
+
+The edit-mode `PercentField` had the same fallback, the placeholder `0`, which beside its `%` adornment read as a filled-in 0%; it now shows the field's `placeholder` or nothing, the same way.
diff --git a/ui/apps/console/src/components/FormPage.currencyPlaceholder-27398.test.tsx b/ui/apps/console/src/components/FormPage.currencyPlaceholder-27398.test.tsx
new file mode 100644
index 00000000000..1a0e36bc986
--- /dev/null
+++ b/ui/apps/console/src/components/FormPage.currencyPlaceholder-27398.test.tsx
@@ -0,0 +1,98 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * objectstack-ai/objectstack#27398 row 2 — on the public form, an empty
+ * optional currency field reads as empty.
+ *
+ * Measured on HotCRM's Contact Us form: the optional Annual Revenue box showed
+ * `0.00` before anyone typed, which reads as an answer. The page hands the
+ * shared `CurrencyField` the row's merged `placeholder`; the widget used to
+ * fall back to `0.00` when that was absent. These pins drive the real public
+ * route (`/f/:slug`) over a served payload.
+ */
+
+import { describe, expect, it, vi, afterEach } from 'vitest';
+import { render, waitFor } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { FormPage } from './FormPage';
+
+vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
+
+const LEAD_SCHEMA = {
+ name: 'crm_lead',
+ label: 'Lead',
+ fields: {
+ company: { type: 'text', label: 'Company', required: true },
+ annual_revenue: { type: 'currency', label: 'Annual Revenue', currency: 'USD' },
+ budget: { type: 'currency', label: 'Budget', currency: 'USD', placeholder: 'Rough figure' },
+ },
+};
+
+function stubFetch(body: unknown) {
+ return vi.fn(async (url: string) => {
+ if (!String(url).includes('/forms/contact-us')) throw new Error(`unstubbed fetch: ${url}`);
+ return {
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ json: async () => body,
+ text: async () => JSON.stringify(body),
+ } as unknown as Response;
+ });
+}
+
+function renderPublic(fields: unknown[]) {
+ vi.stubGlobal(
+ 'fetch',
+ stubFetch({
+ slug: 'contact-us',
+ object: 'crm_lead',
+ form: { type: 'simple', title: 'Contact us', sections: [{ name: 'about', label: 'About you', fields }] },
+ objectSchema: LEAD_SCHEMA,
+ }),
+ );
+ return render(
+
+
+ } />
+
+ ,
+ );
+}
+
+/** The currency box of one row, once its lazy widget has resolved. */
+async function currencyBox(container: HTMLElement, name: string): Promise {
+ let el: HTMLInputElement | null = null;
+ await waitFor(() => {
+ el = container.querySelector(`[data-field="${name}"] input[type=number]`)
+ ?? container.querySelector(`input[type=number][name="${name}"]`);
+ expect(el).not.toBeNull();
+ });
+ return el!;
+}
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+describe('public form — an empty currency field shows no 0.00 stand-in (objectstack#27398 row 2)', () => {
+ it('an optional amount with no placeholder renders an empty box with no placeholder', async () => {
+ const { container } = renderPublic(['company', 'annual_revenue']);
+ const input = await currencyBox(container, 'annual_revenue');
+ expect(input.value).toBe('');
+ expect(input.getAttribute('placeholder') ?? '').toBe('');
+ });
+
+ it("CONTROL — the object field's own placeholder still reaches the box", async () => {
+ const { container } = renderPublic(['budget']);
+ const input = await currencyBox(container, 'budget');
+ expect(input).toHaveAttribute('placeholder', 'Rough figure');
+ });
+
+ it("CONTROL — the form row's placeholder override still wins", async () => {
+ const { container } = renderPublic([{ field: 'annual_revenue', placeholder: 'Optional' }]);
+ const input = await currencyBox(container, 'annual_revenue');
+ expect(input).toHaveAttribute('placeholder', 'Optional');
+ });
+});
diff --git a/ui/packages/fields/src/__tests__/CurrencyField.emptyPlaceholder-27398.test.tsx b/ui/packages/fields/src/__tests__/CurrencyField.emptyPlaceholder-27398.test.tsx
new file mode 100644
index 00000000000..aceee5ccd87
--- /dev/null
+++ b/ui/packages/fields/src/__tests__/CurrencyField.emptyPlaceholder-27398.test.tsx
@@ -0,0 +1,119 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectstack-ai/objectstack#27398 row 2 — an EMPTY currency box reads as
+ * empty.
+ *
+ * The edit-mode input used to fall back to the placeholder `0.00` whenever the
+ * field declared none, so an optional amount nobody filled in (a public form's
+ * Annual Revenue) looked like a filled-in zero. The widget now shows the
+ * field's own `placeholder`, or none — the shape `NumberField` already has.
+ *
+ * Every host draws this one widget with the field it was handed, so the pins
+ * below cover the widget itself and the two package hosts that mount it: the
+ * form renderer (record form) and `FieldEditWidget` (the grid's inline
+ * editor). The public form page's own pin lives beside `FormPage.tsx`.
+ */
+
+import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
+import React from 'react';
+import { render, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { ComponentRegistry } from '@object-ui/core';
+// Module scope: pulls in the form renderer's registration side effect.
+import '@object-ui/components';
+import { LocalizationProvider } from '@object-ui/i18n';
+import { CurrencyField } from '../widgets/CurrencyField';
+import { FieldEditWidget } from '../FieldEditWidget';
+
+beforeAll(() => {
+ ComponentRegistry.register('currency', CurrencyField as any, {
+ namespace: 'field',
+ skipFallback: true,
+ });
+}, 30000);
+
+afterEach(() => cleanup());
+
+const box = (container: HTMLElement): HTMLInputElement => {
+ const el = container.querySelector('input[type=number]');
+ if (!el) throw new Error('no currency input rendered');
+ return el as HTMLInputElement;
+};
+
+/** The attribute as the browser would show it: absent and empty read alike. */
+const shownPlaceholder = (container: HTMLElement) => box(container).getAttribute('placeholder') ?? '';
+
+const renderWidget = (field: Record, value: number | null = null) =>
+ render(
+
+
+ ,
+ );
+
+describe('CurrencyField — an empty box shows no numeric stand-in (objectstack#27398 row 2)', () => {
+ it('an empty field that declares no placeholder shows none', () => {
+ const { container } = renderWidget({ name: 'annual_revenue', currency: 'USD' });
+ expect(box(container).value).toBe('');
+ expect(shownPlaceholder(container)).toBe('');
+ });
+
+ it('a zero-decimal currency is not offered a two-decimal stand-in either', () => {
+ const { container } = renderWidget({ name: 'budget', currency: 'JPY' });
+ expect(shownPlaceholder(container)).toBe('');
+ });
+
+ it('an authored empty placeholder stays empty rather than being replaced', () => {
+ const { container } = renderWidget({ name: 'annual_revenue', currency: 'USD', placeholder: '' });
+ expect(shownPlaceholder(container)).toBe('');
+ });
+
+ it("CONTROL — the field's own placeholder is still shown", () => {
+ const { container } = renderWidget({ name: 'annual_revenue', currency: 'USD', placeholder: 'e.g. 250000' });
+ expect(shownPlaceholder(container)).toBe('e.g. 250000');
+ });
+
+ it('CONTROL — a real zero still shows as a value', () => {
+ const { container } = renderWidget({ name: 'annual_revenue', currency: 'USD' }, 0);
+ expect(box(container).value).toBe('0');
+ });
+});
+
+describe('the hosts that mount CurrencyField hand it no stand-in of their own (objectstack#27398 row 2)', () => {
+ it('the form renderer (record form) draws an empty optional amount with no placeholder', () => {
+ const Form = ComponentRegistry.get('form')!;
+ const { container } = render(
+