diff --git a/.changeset/ui-date-range-picker.md b/.changeset/ui-date-range-picker.md
new file mode 100644
index 0000000000..e69da8d7f9
--- /dev/null
+++ b/.changeset/ui-date-range-picker.md
@@ -0,0 +1,5 @@
+---
+'@backstage/ui': patch
+---
+
+Added new `DateRangePicker` component — combines two date fields and a calendar popover for selecting a date range, built on React Aria with full keyboard and screen reader accessibility. Uses BUI design tokens throughout, including auto-incremented backgrounds via the bg consumer pattern.
diff --git a/docs-ui/src/app/components/date-range-picker/components.tsx b/docs-ui/src/app/components/date-range-picker/components.tsx
new file mode 100644
index 0000000000..3ae30a16ad
--- /dev/null
+++ b/docs-ui/src/app/components/date-range-picker/components.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+import { DateRangePicker } from '../../../../../packages/ui/src/components/DateRangePicker/DateRangePicker';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { parseDate } from '@internationalized/date';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const WithDefaultValue = () => {
+ return (
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ );
+};
diff --git a/docs-ui/src/app/components/date-range-picker/page.mdx b/docs-ui/src/app/components/date-range-picker/page.mdx
new file mode 100644
index 0000000000..53ff5690fb
--- /dev/null
+++ b/docs-ui/src/app/components/date-range-picker/page.mdx
@@ -0,0 +1,82 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { dateRangePickerPropDefs } from './props-definition';
+import {
+ dateRangePickerUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDefaultValueSnippet,
+ disabledSnippet,
+} from './snippets';
+import { WithLabel, Sizes, WithDefaultValue, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { DateRangePickerDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ dateRangePicker: 'https://react-spectrum.adobe.com/react-aria/DateRangePicker.html',
+};
+
+
+
+}
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+}
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With default value
+
+}
+ code={withDefaultValueSnippet}
+ layout="side-by-side"
+/>
+
+### Disabled
+
+}
+ code={disabledSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/date-range-picker/props-definition.tsx b/docs-ui/src/app/components/date-range-picker/props-definition.tsx
new file mode 100644
index 0000000000..b7200f868e
--- /dev/null
+++ b/docs-ui/src/app/components/date-range-picker/props-definition.tsx
@@ -0,0 +1,103 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const dateRangePickerPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the picker. Use small for dense layouts,{' '}
+ medium for prominent fields.
+ >
+ ),
+ },
+ label: {
+ type: 'string',
+ description: 'Visible label displayed above the picker.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary text shown next to the label. If not provided and isRequired
+ is true, displays Required.
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Help text displayed below the label.',
+ },
+ value: {
+ type: 'enum',
+ values: ['RangeValue'],
+ description: 'Controlled value of the date range.',
+ },
+ defaultValue: {
+ type: 'enum',
+ values: ['RangeValue'],
+ description: 'Default value for uncontrolled usage.',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(value: RangeValue | null) => void'],
+ description: 'Handler called when the selected range changes.',
+ },
+ granularity: {
+ type: 'enum',
+ values: ['day', 'hour', 'minute', 'second'],
+ default: 'day',
+ description:
+ 'Smallest unit displayed. Defaults to "day" for dates and "minute" for times.',
+ },
+ minValue: {
+ type: 'enum',
+ values: ['DateValue'],
+ description: 'Minimum allowed date. Dates before this are disabled.',
+ },
+ maxValue: {
+ type: 'enum',
+ values: ['DateValue'],
+ description: 'Maximum allowed date. Dates after this are disabled.',
+ },
+ isDateUnavailable: {
+ type: 'enum',
+ values: ['(date: DateValue) => boolean'],
+ description:
+ 'Callback invoked for each calendar date. Return true to mark a date as unavailable.',
+ },
+ allowsNonContiguousRanges: {
+ type: 'boolean',
+ description:
+ 'When combined with isDateUnavailable, allows selecting ranges that contain unavailable dates.',
+ },
+ startName: {
+ type: 'string',
+ description: 'Form field name for the start date, submitted as ISO 8601.',
+ },
+ endName: {
+ type: 'string',
+ description: 'Form field name for the end date, submitted as ISO 8601.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Whether the field is required for form submission.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the picker is disabled.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Whether the picker is read-only.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/date-range-picker/snippets.ts b/docs-ui/src/app/components/date-range-picker/snippets.ts
new file mode 100644
index 0000000000..3faab626ed
--- /dev/null
+++ b/docs-ui/src/app/components/date-range-picker/snippets.ts
@@ -0,0 +1,28 @@
+export const dateRangePickerUsageSnippet = `import { DateRangePicker } from '@backstage/ui';
+
+`;
+
+export const withLabelSnippet = ``;
+
+export const sizesSnippet = `
+
+
+`;
+
+export const withDefaultValueSnippet = `import { parseDate } from '@internationalized/date';
+
+`;
+
+export const disabledSnippet = ``;
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index 2a16065930..55f10eba95 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -53,6 +53,10 @@ export const components: Page[] = [
title: 'Container',
slug: 'container',
},
+ {
+ title: 'DateRangePicker',
+ slug: 'date-range-picker',
+ },
{
title: 'Dialog',
slug: 'dialog',
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 1b17bc8744..dce45e578b 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -47,6 +47,7 @@
},
"dependencies": {
"@backstage/version-bridge": "workspace:^",
+ "@internationalized/date": "^3.12.0",
"@remixicon/react": "^4.6.0",
"@tanstack/react-table": "^8.21.3",
"clsx": "^2.1.1",
diff --git a/packages/ui/src/components/DateRangePicker/DateRangePicker.module.css b/packages/ui/src/components/DateRangePicker/DateRangePicker.module.css
new file mode 100644
index 0000000000..71f7b6d61e
--- /dev/null
+++ b/packages/ui/src/components/DateRangePicker/DateRangePicker.module.css
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@layer tokens, base, components, utilities;
+
+@layer components {
+ /* ============================================================
+ Root
+ ============================================================ */
+
+ .bui-DateRangePicker {
+ display: flex;
+ flex-direction: column;
+ font-family: var(--bui-font-regular);
+ width: 100%;
+ flex-shrink: 0;
+ }
+
+ /* ============================================================
+ Field group — custom container (not reusing any BUI field)
+ ============================================================ */
+
+ .bui-DateRangePickerGroup {
+ display: flex;
+ align-items: center;
+ background-color: var(--bui-bg-neutral-1);
+ border-radius: var(--bui-radius-2);
+ padding: 0 var(--bui-space-1) 0 var(--bui-space-3);
+ width: fit-content;
+ min-width: 280px;
+ max-width: 100%;
+ overflow: clip;
+ transition: box-shadow 0.2s ease-in-out;
+ cursor: text;
+
+ /* bg consumer — auto-increment background based on parent context */
+ &[data-on-bg='neutral-1'] {
+ background-color: var(--bui-bg-neutral-2);
+ }
+
+ &[data-on-bg='neutral-2'] {
+ background-color: var(--bui-bg-neutral-3);
+ }
+
+ &[data-on-bg='neutral-3'] {
+ background-color: var(--bui-bg-neutral-4);
+ }
+
+ &[data-focus-within] {
+ outline: none;
+ box-shadow: inset 0 0 0 1px var(--bui-ring);
+ }
+
+ &[data-invalid] {
+ box-shadow: inset 0 0 0 1px var(--bui-border-danger);
+ }
+
+ &[data-disabled] {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ /* Sizes */
+ &[data-size='small'] {
+ height: 2rem;
+ padding-inline-start: var(--bui-space-2);
+ }
+
+ &[data-size='medium'] {
+ height: 2.5rem;
+ min-width: 290px;
+ padding-inline-end: var(--bui-space-2);
+ }
+ }
+
+ /* ============================================================
+ Date fields wrapper — scrollable row of inputs + separator
+ ============================================================ */
+
+ .bui-DateRangePickerDateFields {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ width: fit-content;
+ overflow-x: auto;
+ overflow-y: clip;
+ scrollbar-width: none;
+ }
+
+ /* ============================================================
+ Date inputs
+ ============================================================ */
+
+ .bui-DateRangePickerDateInput {
+ display: inline-flex;
+ align-items: center;
+ width: unset;
+ min-width: unset;
+ padding: unset;
+ border: unset;
+ box-shadow: none;
+ background: none;
+ height: auto;
+
+ &[slot='end'] {
+ flex: 1;
+ }
+ }
+
+ /* ============================================================
+ Date segments (month, day, year literals and placeholders)
+ ============================================================ */
+
+ .bui-DateRangePickerSegment {
+ display: inline-block;
+ padding: var(--bui-space-0_5) var(--bui-space-1);
+ border-radius: var(--bui-radius-1);
+ font-size: var(--bui-font-size-3);
+ font-family: var(--bui-font-regular);
+ font-weight: var(--bui-font-weight-regular);
+ color: var(--bui-fg-primary);
+ caret-color: transparent;
+ outline: none;
+ tabular-nums: unset;
+ font-variant-numeric: tabular-nums;
+
+ &[data-placeholder] {
+ color: var(--bui-fg-secondary);
+ }
+
+ &[data-type='literal'] {
+ color: var(--bui-fg-secondary);
+ padding: 0;
+ }
+
+ &[data-focused] {
+ background-color: var(--bui-bg-solid);
+ color: var(--bui-fg-solid);
+ border-radius: var(--bui-radius-1);
+
+ &[data-placeholder] {
+ color: var(--bui-fg-solid);
+ }
+ }
+
+ &[data-disabled] {
+ color: var(--bui-fg-disabled);
+ }
+ }
+
+ /* ============================================================
+ Separator between start and end
+ ============================================================ */
+
+ .bui-DateRangePickerSeparator {
+ color: var(--bui-fg-secondary);
+ font-size: var(--bui-font-size-3);
+ flex-shrink: 0;
+ user-select: none;
+ padding: 0 var(--bui-space-1);
+ }
+
+ /* ============================================================
+ Calendar trigger button
+ ============================================================ */
+
+ .bui-DateRangePickerButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background: none;
+ border: none;
+ padding: var(--bui-space-1);
+ border-radius: var(--bui-radius-1);
+ color: var(--bui-fg-secondary);
+ cursor: pointer;
+ margin-left: auto;
+ transition: color 0.15s ease-in-out;
+
+ &[data-hovered] {
+ color: var(--bui-fg-primary);
+ }
+
+ &[data-focus-visible] {
+ outline: 2px solid var(--bui-ring);
+ outline-offset: 1px;
+ }
+
+ &[data-pressed] {
+ color: var(--bui-fg-primary);
+ }
+ }
+
+ /* ============================================================
+ Calendar (inside Popover)
+ ============================================================ */
+
+ .bui-DateRangePickerCalendar {
+ padding: var(--bui-space-3);
+ width: fit-content;
+ outline: none;
+ }
+
+ .bui-DateRangePickerCalendarHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--bui-space-3);
+ }
+
+ .bui-DateRangePickerCalendarHeading {
+ font-size: var(--bui-font-size-3);
+ font-weight: var(--bui-font-weight-bold);
+ font-family: var(--bui-font-regular);
+ color: var(--bui-fg-primary);
+ flex: 1;
+ text-align: center;
+ }
+
+ .bui-DateRangePickerCalendarNavButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: var(--bui-space-8);
+ height: var(--bui-space-8);
+ background: none;
+ border: none;
+ border-radius: var(--bui-radius-2);
+ color: var(--bui-fg-secondary);
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
+
+ &[data-hovered] {
+ background-color: var(--bui-bg-neutral-2);
+ color: var(--bui-fg-primary);
+ }
+
+ &[data-focus-visible] {
+ outline: 2px solid var(--bui-ring);
+ outline-offset: 1px;
+ }
+
+ &[data-pressed] {
+ background-color: var(--bui-bg-neutral-2);
+ }
+ }
+
+ /* ============================================================
+ Calendar grid
+ ============================================================ */
+
+ .bui-DateRangePickerCalendarGrid {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0 2px;
+ }
+
+ .bui-DateRangePickerCalendarHeaderCell {
+ font-size: var(--bui-font-size-2);
+ font-weight: var(--bui-font-weight-regular);
+ color: var(--bui-fg-secondary);
+ text-align: center;
+ padding-bottom: var(--bui-space-2);
+ width: 40px;
+ }
+
+ /* ============================================================
+ Calendar cells
+
+ Technique: isolation: isolate creates a stacking context so
+ that ::before (range fill, z-index: -2) and ::after (circle,
+ z-index: -1) paint behind the cell's text content while still
+ rendering on top of the page background.
+ ============================================================ */
+
+ .bui-DateRangePickerCalendarCell {
+ isolation: isolate;
+ position: relative;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ outline: none;
+ cursor: default;
+ font-size: var(--bui-font-size-3);
+ font-family: var(--bui-font-regular);
+ color: var(--bui-fg-primary);
+
+ &[data-outside-month] {
+ color: var(--bui-fg-disabled);
+ }
+
+ &[data-disabled] {
+ color: var(--bui-fg-disabled);
+ cursor: not-allowed;
+ }
+
+ &[data-unavailable] {
+ color: var(--bui-fg-disabled);
+ cursor: not-allowed;
+ text-decoration: line-through;
+ }
+
+ &[data-hovered]:not([data-disabled]):not([data-unavailable]):not([data-selected]) {
+ background-color: var(--bui-bg-neutral-2);
+ border-radius: var(--bui-radius-full);
+ }
+
+ &[data-focus-visible] {
+ outline: 2px solid var(--bui-ring);
+ outline-offset: -2px;
+ border-radius: var(--bui-radius-full);
+ }
+
+ /* Today marker */
+ &[data-today]:not([data-selected]):not([data-selection-start]):not([data-selection-end]) {
+ font-weight: var(--bui-font-weight-bold);
+ }
+
+ /* ── Range fill via ::before ─────────────────────────────── */
+
+ &[data-selected]::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: color-mix(in srgb, var(--bui-bg-solid) 15%, transparent);
+ z-index: -2;
+ }
+
+ /* Start cell: fill only the right half to connect forward */
+ &[data-selection-start]::before {
+ left: 50%;
+ }
+
+ /* End cell: fill only the left half to connect backward */
+ &[data-selection-end]::before {
+ right: 50%;
+ }
+
+ /* Same-day selection: no range fill needed */
+ &[data-selection-start][data-selection-end]::before {
+ display: none;
+ }
+
+ /* ── Solid circle for start / end via ::after ────────────── */
+
+ &[data-selection-start]::after,
+ &[data-selection-end]::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: var(--bui-bg-solid);
+ border-radius: var(--bui-radius-full);
+ z-index: -1;
+ }
+
+ /* Text color on top of the solid circle */
+ &[data-selection-start],
+ &[data-selection-end] {
+ color: var(--bui-fg-solid);
+ }
+ }
+
+ /* Round the left edge of ::before when the cell is the first in its row
+ (our class is on an inner element inside
, hence td:first-child > *) */
+ .bui-DateRangePickerCalendarCell[data-selected]:not([data-selection-start]):is(
+ td:first-child > *,
+ [aria-disabled] + td > *
+ )::before {
+ border-start-start-radius: var(--bui-radius-full);
+ border-end-start-radius: var(--bui-radius-full);
+ }
+
+ /* Round the right edge of ::before when the cell is the last in its row */
+ .bui-DateRangePickerCalendarCell[data-selected]:not([data-selection-end]):is(
+ td:last-child > *
+ )::before {
+ border-start-end-radius: var(--bui-radius-full);
+ border-end-end-radius: var(--bui-radius-full);
+ }
+}
diff --git a/packages/ui/src/components/DateRangePicker/DateRangePicker.stories.tsx b/packages/ui/src/components/DateRangePicker/DateRangePicker.stories.tsx
new file mode 100644
index 0000000000..3a596c2b91
--- /dev/null
+++ b/packages/ui/src/components/DateRangePicker/DateRangePicker.stories.tsx
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import preview from '../../../../../.storybook/preview';
+import { DateRangePicker } from './DateRangePicker';
+import { parseDate, today, getLocalTimeZone } from '@internationalized/date';
+import { Form } from 'react-aria-components';
+import { Button } from '../Button';
+
+const meta = preview.meta({
+ title: 'Backstage UI/DateRangePicker',
+ component: DateRangePicker,
+ args: {
+ style: { width: 360 },
+ },
+});
+
+export const Default = meta.story({
+ args: {},
+});
+
+export const WithLabel = meta.story({
+ args: {
+ label: 'Date range',
+ },
+});
+
+export const WithDescription = meta.story({
+ args: {
+ label: 'Date range',
+ description: 'Select a start and end date for your event.',
+ },
+});
+
+export const WithDefaultValue = meta.story({
+ args: {
+ label: 'Booking period',
+ defaultValue: {
+ start: parseDate('2025-02-03'),
+ end: parseDate('2025-02-14'),
+ },
+ },
+});
+
+export const Sizes = meta.story({
+ args: {
+ label: 'Date range',
+ },
+ render: args => (
+
+
+
+
+ ),
+});
+
+export const Required = meta.story({
+ args: {
+ label: 'Trip dates',
+ isRequired: true,
+ },
+ render: args => (
+
+ ),
+});
+
+export const Disabled = meta.story({
+ args: {
+ label: 'Date range',
+ isDisabled: true,
+ defaultValue: {
+ start: parseDate('2025-03-01'),
+ end: parseDate('2025-03-15'),
+ },
+ },
+});
+
+export const Invalid = meta.story({
+ args: {
+ label: 'Date range',
+ isInvalid: true,
+ errorMessage: 'The selected range is not available.',
+ defaultValue: {
+ start: parseDate('2025-04-01'),
+ end: parseDate('2025-04-10'),
+ },
+ },
+});
+
+export const WithMinMaxValue = meta.story({
+ args: {
+ label: 'Date range',
+ description: 'You can only select dates within the next 30 days.',
+ minValue: today(getLocalTimeZone()),
+ maxValue: today(getLocalTimeZone()).add({ days: 30 }),
+ },
+});
diff --git a/packages/ui/src/components/DateRangePicker/DateRangePicker.tsx b/packages/ui/src/components/DateRangePicker/DateRangePicker.tsx
new file mode 100644
index 0000000000..0daec0fb56
--- /dev/null
+++ b/packages/ui/src/components/DateRangePicker/DateRangePicker.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { forwardRef, useEffect } from 'react';
+import {
+ DateRangePicker as AriaDateRangePicker,
+ Popover,
+} from 'react-aria-components';
+import clsx from 'clsx';
+import { FieldLabel } from '../FieldLabel';
+import { FieldError } from '../FieldError';
+import { DateRangePickerGroup } from './DateRangePickerGroup';
+import { DateRangePickerCalendar } from './DateRangePickerCalendar';
+import { useDefinition } from '../../hooks/useDefinition';
+import { DateRangePickerDefinition } from './definition';
+import { PopoverDefinition } from '../Popover/definition';
+import type { DateRangePickerProps } from './types';
+
+/**
+ * A date range picker that combines two date fields and a calendar popover,
+ * allowing users to enter or select a date range with full keyboard and
+ * screen reader accessibility.
+ *
+ * @public
+ */
+export const DateRangePicker = forwardRef(
+ (props, ref) => {
+ const { ownProps, restProps, dataAttributes } = useDefinition(
+ DateRangePickerDefinition,
+ props,
+ );
+ const { ownProps: popoverOwnProps } = useDefinition(PopoverDefinition, {});
+
+ const { classes, label, description, secondaryLabel } = ownProps;
+
+ const ariaLabel = restProps['aria-label'];
+ const ariaLabelledBy = restProps['aria-labelledby'];
+
+ useEffect(() => {
+ if (!label && !ariaLabel && !ariaLabelledBy) {
+ console.warn(
+ 'DateRangePicker requires either a visible label, aria-label, or aria-labelledby for accessibility',
+ );
+ }
+ }, [label, ariaLabel, ariaLabelledBy]);
+
+ const secondaryLabelText =
+ secondaryLabel || (restProps.isRequired ? 'Required' : null);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+DateRangePicker.displayName = 'DateRangePicker';
diff --git a/packages/ui/src/components/DateRangePicker/DateRangePickerCalendar.tsx b/packages/ui/src/components/DateRangePicker/DateRangePickerCalendar.tsx
new file mode 100644
index 0000000000..dbfdc4d1d3
--- /dev/null
+++ b/packages/ui/src/components/DateRangePicker/DateRangePickerCalendar.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ RangeCalendar,
+ CalendarGrid,
+ CalendarGridHeader,
+ CalendarHeaderCell,
+ CalendarGridBody,
+ CalendarCell,
+ Heading,
+ Button,
+} from 'react-aria-components';
+import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react';
+import { useDefinition } from '../../hooks/useDefinition';
+import { DateRangePickerCalendarDefinition } from './definition';
+
+/**
+ * Calendar popover content for DateRangePicker — renders the RangeCalendar
+ * with navigation and a full calendar grid.
+ *
+ * @internal
+ */
+export const DateRangePickerCalendar = () => {
+ const { ownProps } = useDefinition(DateRangePickerCalendarDefinition, {});
+ const { classes } = ownProps;
+
+ return (
+
+
+
+
+
+
+
+
+ {day => (
+
+ {day}
+
+ )}
+
+
+ {date => }
+
+
+
+ );
+};
diff --git a/packages/ui/src/components/DateRangePicker/DateRangePickerGroup.tsx b/packages/ui/src/components/DateRangePicker/DateRangePickerGroup.tsx
new file mode 100644
index 0000000000..a5039563b1
--- /dev/null
+++ b/packages/ui/src/components/DateRangePicker/DateRangePickerGroup.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Group, DateInput, DateSegment, Button } from 'react-aria-components';
+import { RiCalendarLine } from '@remixicon/react';
+import { useDefinition } from '../../hooks/useDefinition';
+import { DateRangePickerGroupDefinition } from './definition';
+
+/**
+ * Custom field group for DateRangePicker — renders two DateInput fields,
+ * a separator, and a calendar trigger button.
+ *
+ * @internal
+ */
+export const DateRangePickerGroup = ({ dataSize }: { dataSize?: string }) => {
+ const { ownProps, dataAttributes } = useDefinition(
+ DateRangePickerGroupDefinition,
+ {},
+ );
+ const { classes } = ownProps;
+
+ return (
+
+