diff --git a/packages/fields/src/widgets/GridField.test.tsx b/packages/fields/src/widgets/GridField.test.tsx
index 107ad5fff2..a5f8c44755 100644
--- a/packages/fields/src/widgets/GridField.test.tsx
+++ b/packages/fields/src/widgets/GridField.test.tsx
@@ -113,6 +113,62 @@ describe('GridField / LineItemsField — editable line items', () => {
expect(onChange).toHaveBeenCalledWith([{ description: 'Taxi', amount: 42.5 }]);
});
+ /**
+ * objectui#3566 — the sub-grid face of #3127. `` accepts
+ * exactly `YYYY-MM-DD` and rejects every other shape SILENTLY: the attribute
+ * still lands in the DOM, so the value looks present in the markup, while
+ * `.value` reads back `''` and the cell paints its empty placeholder. Since
+ * the API hands back `2026-06-17T00:00:00.000Z` for a `date` field, every
+ * date cell in an inline child grid rendered blank on a row that has a value.
+ * Asserting `.value` (never `getAttribute('value')`) is the only way to see
+ * this bug at all.
+ */
+ describe('date columns echo the stored value (#3566)', () => {
+ const dateField = {
+ columns: [
+ { field: 'description', label: 'Description', type: 'text' as const },
+ { field: 'incurred_on', label: 'Incurred On', type: 'date' as const },
+ ],
+ } as any;
+
+ // Both spellings the API may hand back for a `date` field. The bare form
+ // must survive VERBATIM — re-parsing it as UTC midnight and reading local
+ // calendar components back out would move it to the 16th anywhere west of
+ // Greenwich, which is the off-by-one this fix must not introduce.
+ it.each(['2026-06-17', '2026-06-17T00:00:00.000Z'])(
+ 'shows the stored calendar day for %s instead of an empty cell',
+ (stored) => {
+ render(
+ {}} field={dateField} />,
+ );
+ const cell = screen.getAllByLabelText('Incurred On')[0] as HTMLInputElement;
+ expect(cell.type).toBe('date');
+ expect(cell.value).toBe('2026-06-17');
+ },
+ );
+
+ it('leaves an empty date cell empty', () => {
+ render( {}} field={dateField} />);
+ expect((screen.getAllByLabelText('Incurred On')[0] as HTMLInputElement).value).toBe('');
+ });
+
+ it('writes back the control\'s own YYYY-MM-DD, unchanged by the read fix', () => {
+ // No paired conversion on the write side: `onChange` hands over exactly
+ // what `` produces, matching DateField's contract. A
+ // conversion here would put read and write on different bases.
+ const onChange = vi.fn();
+ render(
+ ,
+ );
+ fireEvent.change(screen.getAllByLabelText('Incurred On')[0], { target: { value: '2026-06-18' } });
+ expect(onChange).toHaveBeenCalledWith([{ description: 'Taxi', incurred_on: '2026-06-18' }]);
+ });
+ });
+
describe('trailing ghost row (start-with-one + auto-append)', () => {
it('renders a trailing empty row so an empty grid still has one input line', () => {
render( {}} field={field} />);
diff --git a/packages/fields/src/widgets/GridField.tsx b/packages/fields/src/widgets/GridField.tsx
index 77c34d9411..453d32bbe2 100644
--- a/packages/fields/src/widgets/GridField.tsx
+++ b/packages/fields/src/widgets/GridField.tsx
@@ -19,6 +19,7 @@ import { Plus, Trash2, SlidersHorizontal, Maximize2, Copy, GripVertical } from '
import { resolveFieldRuleState } from '@object-ui/core';
import { LookupField } from './LookupField';
import { FileCell } from './FileField';
+import { toDateInputValue } from './nativeDateValue';
/**
* GridField / LineItemsField — editable child-grid ("line items") widget.
@@ -760,7 +761,13 @@ export function GridField({
type={c.type === 'date' ? 'date' : isNumeric(c.type) ? 'number' : 'text'}
step={isNumeric(c.type) ? c.step ?? 'any' : undefined}
aria-label={c.label || c.field}
- value={val != null ? String(val) : ''}
+ // A `date` cell holding the API's ISO shape (`2026-06-17T00:00:00.000Z`)
+ // is SILENTLY rejected by `` — the attribute lands in
+ // the DOM but `input.value` reads back `''` and the cell paints empty
+ // (objectui#3566, the sub-grid face of #3127). The write-back shape is
+ // unchanged: the control's own plain `YYYY-MM-DD`, so no paired
+ // conversion is needed on the `onChange` side.
+ value={c.type === 'date' ? toDateInputValue(val) : val != null ? String(val) : ''}
onChange={(e) => setCell(rowIdx, c, e.target.value)}
disabled={locked}
/>