+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ErrorListTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ErrorListTemplate.tsx
new file mode 100644
index 0000000000..9f9d10a66a
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ErrorListTemplate.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026 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 {
+ ErrorListProps,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ TranslatableString,
+} from '@rjsf/utils';
+import { Box, Text } from '@backstage/ui';
+
+export default function ErrorListTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({ errors, registry }: ErrorListProps) {
+ const { translateString } = registry;
+
+ if (errors.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {translateString(TranslatableString.ErrorsLabel)}
+
+
+ {errors.map((error, i) => (
+
+
+ {error.stack}
+
+
+ ))}
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldErrorTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldErrorTemplate.tsx
new file mode 100644
index 0000000000..fcce513dac
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldErrorTemplate.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 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 {
+ errorId,
+ FieldErrorProps,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Text } from '@backstage/ui';
+
+export default function FieldErrorTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({ errors = [], idSchema }: FieldErrorProps) {
+ if (errors.length === 0) {
+ return null;
+ }
+
+ const id = errorId(idSchema);
+
+ return (
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldHelpTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldHelpTemplate.tsx
new file mode 100644
index 0000000000..66c2343dfd
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldHelpTemplate.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 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 { isValidElement } from 'react';
+import {
+ FieldHelpProps,
+ FormContextType,
+ helpId,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Text } from '@backstage/ui';
+
+export default function FieldHelpTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({ idSchema, help }: FieldHelpProps) {
+ if (!help) {
+ return null;
+ }
+
+ const id = helpId(idSchema);
+
+ return (
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.test.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.test.tsx
new file mode 100644
index 0000000000..1beac36bb4
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022 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 { render } from '@testing-library/react';
+import validator from '@rjsf/validator-ajv8';
+import { Form } from '../../Form';
+
+
+describe('BUI FieldTemplate', () => {
+ it('should render the field label', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Full Name')).toBeInTheDocument();
+ });
+
+ it('should show Required when field is required', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText(/Required/)).toBeInTheDocument();
+ });
+
+ it('should render the description as markdown', () => {
+ const { container } = render(
+ ,
+ );
+
+ const strong = container.querySelector('strong');
+ expect(strong).toBeInTheDocument();
+ expect(strong?.textContent).toBe('full');
+ });
+
+ it('should hide the field when ui:widget is hidden', () => {
+ const { container } = render(
+ ,
+ );
+
+ // Hidden fields should not have a visible input
+ const inputs = container.querySelectorAll('input[type="text"]');
+ expect(inputs.length).toBe(0);
+ });
+
+ it('should render validation errors', () => {
+ const { getAllByText } = render(
+ ,
+ );
+
+ // The error shows up both in the top-level ErrorListTemplate and inline via FieldErrorTemplate
+ const errorMessages = getAllByText(
+ /must NOT have fewer than 3 characters/i,
+ );
+ expect(errorMessages.length).toBeGreaterThanOrEqual(1);
+ });
+});
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.tsx
new file mode 100644
index 0000000000..ac9452ef46
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/FieldTemplate.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022 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 {
+ FieldTemplateProps,
+ FormContextType,
+ getTemplate,
+ getUiOptions,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Box } from '@backstage/ui';
+import { ScaffolderField } from '../../../ScaffolderField';
+
+export default function FieldTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: FieldTemplateProps) {
+ const {
+ id,
+ children,
+ classNames,
+ style,
+ disabled,
+ displayLabel,
+ hidden,
+ label,
+ onDropPropertyClick,
+ onKeyChange,
+ readonly,
+ registry,
+ required,
+ rawErrors = [],
+ errors,
+ help,
+ rawDescription,
+ schema,
+ uiSchema,
+ } = props;
+
+ const uiOptions = getUiOptions(uiSchema);
+ const WrapIfAdditionalTemplate = getTemplate<
+ 'WrapIfAdditionalTemplate',
+ T,
+ S,
+ F
+ >('WrapIfAdditionalTemplate', registry, uiOptions);
+
+ if (hidden) {
+ return {children};
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ObjectFieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ObjectFieldTemplate.tsx
new file mode 100644
index 0000000000..214d405118
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/ObjectFieldTemplate.tsx
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 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 {
+ canExpand,
+ FormContextType,
+ getTemplate,
+ getUiOptions,
+ ObjectFieldTemplateProps,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Box } from '@backstage/ui';
+
+export default function ObjectFieldTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ description,
+ title,
+ properties,
+ required,
+ disabled,
+ readonly,
+ uiSchema,
+ idSchema,
+ schema,
+ formData,
+ onAddClick,
+ registry,
+}: ObjectFieldTemplateProps) {
+ const uiOptions = getUiOptions(uiSchema);
+ const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>(
+ 'TitleFieldTemplate',
+ registry,
+ uiOptions,
+ );
+ const DescriptionFieldTemplate = getTemplate<
+ 'DescriptionFieldTemplate',
+ T,
+ S,
+ F
+ >('DescriptionFieldTemplate', registry, uiOptions);
+ const {
+ ButtonTemplates: { AddButton },
+ } = registry.templates;
+
+ const isNested =
+ idSchema.$id !== 'root' &&
+ (idSchema.$id.includes('.') || idSchema.$id.includes('_'));
+
+ const isRoot = idSchema.$id === 'root';
+ const hasTitle = !isRoot && (uiOptions.title || title);
+ const hasDescription = !isRoot && (uiOptions.description || description);
+
+ const isAutoGeneratedTitle = title && /^.+-\d+$/.test(title);
+
+ if (isNested || isAutoGeneratedTitle) {
+ return (
+
+ {properties.map(element => element.content)}
+ {canExpand(schema, uiSchema, formData) && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {hasTitle && (
+
+ )}
+ {hasDescription && (
+
+ )}
+ {properties.map(element => element.content)}
+ {canExpand(schema, uiSchema, formData) && (
+
+
+
+ )}
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/TitleFieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/TitleFieldTemplate.tsx
new file mode 100644
index 0000000000..7b9d60ead0
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/TitleFieldTemplate.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 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 {
+ FormContextType,
+ TitleFieldProps,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Box, Text } from '@backstage/ui';
+
+export default function TitleFieldTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({ id, title, required }: TitleFieldProps) {
+ return (
+
+
+ {title}
+ {required && (
+
+ {' '}
+ *
+
+ )}
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/UnsupportedFieldTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/UnsupportedFieldTemplate.tsx
new file mode 100644
index 0000000000..8515832dc0
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/UnsupportedFieldTemplate.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 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 {
+ UnsupportedFieldProps,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { Box, Text } from '@backstage/ui';
+
+export default function UnsupportedFieldTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: UnsupportedFieldProps) {
+ const { schema, reason } = props;
+ return (
+
+
+ Unsupported field schema
+ {schema && schema.type && ` for type: ${JSON.stringify(schema.type)}`}
+ {reason && `: ${reason}`}
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/WrapIfAdditionalTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/WrapIfAdditionalTemplate.tsx
new file mode 100644
index 0000000000..c5241fcf05
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/WrapIfAdditionalTemplate.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2026 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 {
+ ADDITIONAL_PROPERTY_FLAG,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WrapIfAdditionalTemplateProps,
+} from '@rjsf/utils';
+import { Button, Flex, Box, TextField } from '@backstage/ui';
+import { RiDeleteBinLine } from '@remixicon/react';
+
+export default function WrapIfAdditionalTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ children,
+ classNames,
+ style,
+ disabled,
+ id,
+ label,
+ onDropPropertyClick,
+ onKeyChange,
+ readonly,
+ required,
+ schema,
+}: WrapIfAdditionalTemplateProps) {
+ const keyLabel = `${label} Key`;
+ const additional = ADDITIONAL_PROPERTY_FLAG in schema;
+
+ if (!additional) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const handleChange = (value: string) => {
+ onKeyChange(value);
+ };
+
+ return (
+
+
+
+
+
+
+ {children}
+
+ }
+ aria-label="Remove"
+ />
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/index.ts b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/index.ts
new file mode 100644
index 0000000000..a271e52c47
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/templates/index.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ TemplatesType,
+} from '@rjsf/utils';
+
+import ArrayFieldDescriptionTemplate from './ArrayFieldDescriptionTemplate';
+import ArrayFieldItemTemplate from './ArrayFieldItemTemplate';
+import ArrayFieldTemplate from './ArrayFieldTemplate';
+import ArrayFieldTitleTemplate from './ArrayFieldTitleTemplate';
+import ButtonTemplates, {
+ SubmitButton,
+ AddButton,
+ IconButton,
+} from './ButtonTemplates';
+import DescriptionFieldTemplate from './DescriptionFieldTemplate';
+import ErrorListTemplate from './ErrorListTemplate';
+import FieldErrorTemplate from './FieldErrorTemplate';
+import FieldHelpTemplate from './FieldHelpTemplate';
+import FieldTemplate from './FieldTemplate';
+import ObjectFieldTemplate from './ObjectFieldTemplate';
+import TitleFieldTemplate from './TitleFieldTemplate';
+import UnsupportedFieldTemplate from './UnsupportedFieldTemplate';
+import WrapIfAdditionalTemplate from './WrapIfAdditionalTemplate';
+
+export function generateBuiTemplates<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(): Partial> {
+ return {
+ ArrayFieldDescriptionTemplate,
+ ArrayFieldItemTemplate,
+ ArrayFieldTemplate,
+ ArrayFieldTitleTemplate,
+ ButtonTemplates: {
+ ...ButtonTemplates,
+ SubmitButton,
+ AddButton,
+ RemoveButton: IconButton,
+ MoveDownButton: IconButton,
+ MoveUpButton: IconButton,
+ CopyButton: IconButton,
+ },
+ DescriptionFieldTemplate,
+ ErrorListTemplate,
+ FieldErrorTemplate,
+ FieldHelpTemplate,
+ FieldTemplate,
+ ObjectFieldTemplate,
+ TitleFieldTemplate,
+ UnsupportedFieldTemplate,
+ WrapIfAdditionalTemplate,
+ };
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.test.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.test.tsx
new file mode 100644
index 0000000000..2affd34dee
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 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 { render, fireEvent, act } from '@testing-library/react';
+import validator from '@rjsf/validator-ajv8';
+import { Form } from '../../Form';
+
+describe('BUI BaseInputTemplate', () => {
+ it('should render a text input', () => {
+ const { container } = render(
+ ,
+ );
+
+ const input = container.querySelector('input');
+ expect(input).toBeInTheDocument();
+ });
+
+ it('should handle onChange', async () => {
+ const onChange = jest.fn();
+
+ const { container } = render(
+ ,
+ );
+
+ const input = container.querySelector('input')!;
+
+ await act(async () => {
+ fireEvent.change(input, { target: { value: 'hello' } });
+ });
+
+ expect(onChange).toHaveBeenCalled();
+ const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
+ expect(lastCall.formData).toEqual({ name: 'hello' });
+ });
+
+ it('should show invalid state when there are validation errors', async () => {
+ const { container } = render(
+ ,
+ );
+
+ const input = container.querySelector('input')!;
+ expect(input).toHaveAttribute('aria-invalid', 'true');
+ });
+
+ it('should render label via FieldTemplate, not the widget itself', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('My Field Title')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.tsx
new file mode 100644
index 0000000000..90377573be
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/BaseInputTemplate.tsx
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2026 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 {
+ ariaDescribedByIds,
+ BaseInputTemplateProps,
+ examplesId,
+ FormContextType,
+ getInputProps,
+ RJSFSchema,
+ StrictRJSFSchema,
+} from '@rjsf/utils';
+import { TextField } from '@backstage/ui';
+
+export default function BaseInputTemplate<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ placeholder,
+ required,
+ readonly,
+ disabled,
+ type,
+ label,
+ value,
+ onChange: onFieldChange,
+ onBlur: onFieldBlur,
+ onFocus: onFieldFocus,
+ autofocus,
+ options,
+ schema,
+ rawErrors = [],
+ children,
+ extraProps,
+}: BaseInputTemplateProps) {
+ const { type: inputType, ...restInputProps } = {
+ ...extraProps,
+ ...getInputProps(schema, type, options),
+ };
+
+ const list = schema.examples ? examplesId(id) : undefined;
+
+ const handleChange = (val: string) =>
+ onFieldChange(val === '' ? options.emptyValue : val);
+ const handleBlur = () => onFieldBlur(id, value);
+ const handleFocus = () => onFieldFocus(id, value);
+
+ const hasError = rawErrors.length > 0;
+ const isNumeric = inputType === 'number' || inputType === 'integer';
+
+ // Normalize value to string for the input
+ let inputValue: string;
+ if (isNumeric) {
+ inputValue = value || value === 0 ? String(value) : '';
+ } else {
+ inputValue = value === null || value === undefined ? '' : String(value);
+ }
+
+ return (
+ <>
+ (id, !!schema.examples)}
+ list={list}
+ {...restInputProps}
+ />
+ {children}
+ {Array.isArray(schema.examples) ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxWidget.tsx
new file mode 100644
index 0000000000..35470a9c9a
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxWidget.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ labelValue,
+ RJSFSchema,
+ schemaRequiresTrueValue,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { Checkbox } from '@backstage/ui';
+
+export default function CheckboxWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: WidgetProps) {
+ const {
+ id,
+ value,
+ disabled,
+ readonly,
+ label,
+ hideLabel,
+ schema,
+ onChange: onFieldChange,
+ } = props;
+
+ const required = schemaRequiresTrueValue(schema);
+
+ const handleChange = (checked: boolean) => {
+ onFieldChange(checked);
+ };
+
+ return (
+
+ {labelValue(label, hideLabel || !label)}
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxesWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxesWidget.tsx
new file mode 100644
index 0000000000..54e15fdd0a
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/CheckboxesWidget.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2026 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 {
+ ariaDescribedByIds,
+ FormContextType,
+ optionId,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { Checkbox, CheckboxGroup } from '@backstage/ui';
+
+export default function CheckboxesWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ label,
+ required,
+ disabled,
+ readonly,
+ options,
+ value,
+ onChange: onFieldChange,
+ rawErrors = [],
+}: WidgetProps) {
+ const { enumOptions, enumDisabled } = options;
+
+ const selectedKeys = Array.isArray(value) ? value.map(String) : [];
+ const hasError = rawErrors.length > 0;
+
+ const handleChange = (keys: string[]) => {
+ const next =
+ enumOptions
+ ?.filter(option => keys.includes(String(option.value)))
+ .map(option => option.value) ?? [];
+ onFieldChange(next as T);
+ };
+
+ return (
+ (id)}
+ >
+ {enumOptions?.map((option, index) => {
+ const itemDisabled =
+ Array.isArray(enumDisabled) &&
+ enumDisabled.indexOf(option.value) !== -1;
+ return (
+
+ {option.label}
+
+ );
+ })}
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/ColorWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/ColorWidget.tsx
new file mode 100644
index 0000000000..d496b6a405
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/ColorWidget.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import BaseInputTemplate from './BaseInputTemplate';
+
+export default function ColorWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: WidgetProps) {
+ return ;
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateInput.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateInput.tsx
new file mode 100644
index 0000000000..6a3f4904ed
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateInput.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2026 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 { CSSProperties, ChangeEvent } from 'react';
+import {
+ ariaDescribedByIds,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { FieldLabel } from '@backstage/ui';
+
+// Native fallback until BUI ships DatePicker/TimePicker components.
+const baseStyle: CSSProperties = {
+ display: 'flex',
+ alignItems: 'center',
+ height: '2rem',
+ padding: '0 var(--bui-space-3)',
+ borderRadius: 'var(--bui-radius-2)',
+ border: '1px solid var(--bui-border-2)',
+ backgroundColor: 'var(--bui-bg-neutral-1)',
+ fontSize: 'var(--bui-font-size-3)',
+ fontFamily: 'var(--bui-font-regular)',
+ fontWeight: 400,
+ color: 'var(--bui-fg-primary)',
+ transition: 'border-color 0.2s ease-in-out',
+ width: '100%',
+ boxSizing: 'border-box' as const,
+ outline: 'none',
+ WebkitAppearance: 'none' as const,
+ MozAppearance: 'none' as const,
+ appearance: 'none' as const,
+};
+
+interface DateInputProps<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+> extends WidgetProps {
+ inputType: 'date' | 'datetime-local' | 'time';
+}
+
+export default function DateInput<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ label,
+ required,
+ disabled,
+ readonly,
+ value,
+ autofocus,
+ schema,
+ options = {},
+ onChange: onFieldChange,
+ onBlur: onFieldBlur,
+ onFocus: onFieldFocus,
+ rawErrors = [],
+ inputType,
+}: DateInputProps) {
+ const handleChange = (event: ChangeEvent) =>
+ onFieldChange(
+ event.target.value === '' ? options.emptyValue : event.target.value,
+ );
+ const handleBlur = () => onFieldBlur(id, value);
+ const handleFocus = () => onFieldFocus(id, value);
+
+ const hasError = rawErrors.length > 0;
+ const stringValue = String(value ?? '');
+
+ return (
+ <>
+ {(label || schema.title) && (
+
+ )}
+ (id)}
+ aria-invalid={hasError}
+ style={{
+ ...baseStyle,
+ ...(disabled && {
+ opacity: 0.5,
+ cursor: 'not-allowed',
+ borderColor: 'var(--bui-border-disabled)',
+ }),
+ ...(hasError && {
+ borderColor: 'var(--bui-fg-danger)',
+ }),
+ }}
+ />
+ >
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateTimeWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateTimeWidget.tsx
new file mode 100644
index 0000000000..dfeb5c07b6
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateTimeWidget.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import DateInput from './DateInput';
+
+export default function DateTimeWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: WidgetProps) {
+ return ;
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateWidget.tsx
new file mode 100644
index 0000000000..6636951b83
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/DateWidget.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import DateInput from './DateInput';
+
+export default function DateWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: WidgetProps) {
+ return ;
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/EmailWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/EmailWidget.tsx
new file mode 100644
index 0000000000..b40e79780e
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/EmailWidget.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import BaseInputTemplate from './BaseInputTemplate';
+
+export default function EmailWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>(props: WidgetProps) {
+ return ;
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/FileWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/FileWidget.tsx
new file mode 100644
index 0000000000..6bbf175955
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/FileWidget.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2026 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 { ChangeEvent, useCallback } from 'react';
+import {
+ ariaDescribedByIds,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+
+function processFile(
+ file: File,
+): Promise<{ dataURL: string; name: string; size: number; type: string }> {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve({
+ dataURL: reader.result as string,
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ });
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
+function processFiles(files: FileList): Promise {
+ return Promise.all(Array.from(files).map(processFile));
+}
+
+export default function FileWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ readonly,
+ disabled,
+ required,
+ multiple,
+ onChange,
+ options,
+}: WidgetProps) {
+ const handleChange = useCallback(
+ (event: ChangeEvent) => {
+ if (!event.target.files || readonly || disabled) {
+ return;
+ }
+ processFiles(event.target.files).then(filesInfo => {
+ onChange(multiple ? filesInfo : filesInfo[0]);
+ });
+ },
+ [multiple, readonly, disabled, onChange],
+ );
+
+ return (
+ (id)}
+ />
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/HiddenWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/HiddenWidget.tsx
new file mode 100644
index 0000000000..b0738c7ff3
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/HiddenWidget.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 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 {
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+
+export default function HiddenWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({ id, value }: WidgetProps) {
+ return (
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/PasswordWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/PasswordWidget.tsx
new file mode 100644
index 0000000000..1df6aabd63
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/PasswordWidget.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2026 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 {
+ ariaDescribedByIds,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { PasswordField } from '@backstage/ui';
+
+export default function PasswordWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ placeholder,
+ required,
+ readonly,
+ disabled,
+ label,
+ value,
+ onChange: onFieldChange,
+ onBlur: onFieldBlur,
+ onFocus: onFieldFocus,
+ autofocus,
+ options,
+ schema,
+ rawErrors = [],
+}: WidgetProps) {
+ const handleChange = (val: string) =>
+ onFieldChange(val === '' ? options.emptyValue : val);
+ const handleBlur = () => onFieldBlur(id, value);
+ const handleFocus = () => onFieldFocus(id, value);
+
+ const hasError = rawErrors.length > 0;
+
+ return (
+ (id)}
+ />
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RadioWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RadioWidget.tsx
new file mode 100644
index 0000000000..108aa006ce
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RadioWidget.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 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 {
+ ariaDescribedByIds,
+ enumOptionsIndexForValue,
+ enumOptionsValueForIndex,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { RadioGroup, Radio } from '@backstage/ui';
+
+export default function RadioWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ options,
+ required,
+ value,
+ disabled,
+ readonly,
+ label,
+ onChange: onFieldChange,
+ rawErrors = [],
+}: WidgetProps) {
+ const { enumOptions, enumDisabled, emptyValue } = options;
+
+ const handleChange = (newValue: string) => {
+ const actualValue = enumOptionsValueForIndex(
+ newValue,
+ enumOptions,
+ emptyValue,
+ );
+ onFieldChange(actualValue);
+ };
+
+ const selectedIndex =
+ enumOptionsIndexForValue(value, enumOptions) ?? undefined;
+ const hasError = rawErrors.length > 0;
+
+ return (
+ (id)}
+ >
+ {enumOptions?.map(({ value: optionValue, label: optionLabel }, index) => {
+ const itemDisabled =
+ Array.isArray(enumDisabled) &&
+ enumDisabled.indexOf(optionValue) !== -1;
+
+ return (
+
+ {optionLabel}
+
+ );
+ })}
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RangeWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RangeWidget.tsx
new file mode 100644
index 0000000000..1a3f0cb12f
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/RangeWidget.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2026 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 { ChangeEvent } from 'react';
+import {
+ ariaDescribedByIds,
+ FormContextType,
+ rangeSpec,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { Flex, Text } from '@backstage/ui';
+
+export default function RangeWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ value,
+ readonly,
+ disabled,
+ schema,
+ label,
+ required,
+ onChange: onFieldChange,
+ onBlur: onFieldBlur,
+ onFocus: onFieldFocus,
+}: WidgetProps) {
+ const { min, max, step } = rangeSpec(schema);
+
+ const handleChange = ({
+ target: { value: inputValue },
+ }: ChangeEvent) => onFieldChange(parseFloat(inputValue));
+ const handleBlur = () => onFieldBlur(id, value);
+ const handleFocus = () => onFieldFocus(id, value);
+
+ return (
+
+ (id)}
+ required={required}
+ style={{ flex: 1 }}
+ />
+
+ {value ?? min}
+
+
+ );
+}
diff --git a/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/SelectWidget.tsx b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/SelectWidget.tsx
new file mode 100644
index 0000000000..6e2f26b97a
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/Form/BuiTheme/widgets/SelectWidget.tsx
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2026 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 {
+ ariaDescribedByIds,
+ enumOptionsIndexForValue,
+ enumOptionsValueForIndex,
+ FormContextType,
+ RJSFSchema,
+ StrictRJSFSchema,
+ WidgetProps,
+} from '@rjsf/utils';
+import { Select } from '@backstage/ui';
+import overrides from './selectOverrides.module.css';
+
+export default function SelectWidget<
+ T = any,
+ S extends StrictRJSFSchema = RJSFSchema,
+ F extends FormContextType = any,
+>({
+ id,
+ options,
+ required,
+ disabled,
+ readonly,
+ value,
+ multiple,
+ label,
+ schema,
+ onChange: onFieldChange,
+ onBlur: onFieldBlur,
+ onFocus: onFieldFocus,
+ rawErrors = [],
+}: WidgetProps) {
+ const { enumOptions, emptyValue } = options;
+
+ const handleChange = (newValue: string) => {
+ const actualIndex = newValue === '' ? '' : newValue;
+ onFieldChange(
+ enumOptionsValueForIndex(actualIndex, enumOptions, emptyValue),
+ );
+ };
+
+ const handleBlur = () =>
+ onFieldBlur(
+ id,
+ enumOptionsValueForIndex(value, enumOptions, emptyValue),
+ );
+ const handleFocus = () =>
+ onFieldFocus(
+ id,
+ enumOptionsValueForIndex(value, enumOptions, emptyValue),
+ );
+
+ const selectedIndex = enumOptionsIndexForValue(value, enumOptions);
+ const hasError = rawErrors.length > 0;
+
+ const selectOptions =
+ enumOptions?.map(({ label: optionLabel }, index) => ({
+ value: String(index),
+ label: optionLabel,
+ })) || [];
+
+ if (!multiple && schema.default === undefined) {
+ selectOptions.unshift({
+ value: '',
+ label: (options.placeholder as string) || 'Select an option',
+ });
+ }
+
+ return (
+