+ );
+}
diff --git a/packages/react-components/src/utils/createComponent.ts b/packages/react-components/src/utils/createComponent.ts
index 74fe3659..0b01fd64 100644
--- a/packages/react-components/src/utils/createComponent.ts
+++ b/packages/react-components/src/utils/createComponent.ts
@@ -60,10 +60,7 @@ type ComponentProps = Omit<
EventListeners &
ElementProps;
-export type ThemedWebComponentProps<
- I extends ThemePropertyMixinClass & HTMLElement,
- E extends EventNames = {},
-> = ComponentProps & {
+export type ThemedWebComponentProps = ComponentProps & {
/**
* Attribute that can be used by the component to apply built-in style variants,
* or to propagate its value to the sub-components in Shadow DOM.
@@ -79,11 +76,28 @@ type AllWebComponentProps = I
export type WebComponentProps = Partial>;
+/**
+ * The type of a React component created from a Vaadin web component.
+ */
+export type ReactWebComponent = (
+ props: WebComponentProps & RefAttributes,
+) => React.ReactElement | null;
+
+/**
+ * The type of a React component that always accepts a `theme` property, even if
+ * the underlying web component does not use `ThemableMixin` / does not expose
+ * `ThemePropertyMixinClass` in its type. Used for components that support theme
+ * variants through the `theme` attribute without the mixin.
+ */
+export type ThemedReactWebComponent = (
+ props: Partial> & RefAttributes,
+) => React.ReactElement | null;
+
// We need a separate declaration here; otherwise, the TypeScript fails into the
// endless loop trying to resolve the typings.
export function createComponent(
options: Options,
-): (props: WebComponentProps & RefAttributes) => React.ReactElement | null;
+): ReactWebComponent;
export function createComponent(options: Options): any {
const { elementClass } = options;
@@ -106,3 +120,18 @@ export function createComponent(
+ options: Options,
+): ThemedReactWebComponent;
+
+// Creates a React component that always accepts a `theme` property, regardless of
+// whether the underlying web component uses `ThemableMixin`. This only widens the
+// type: the runtime is identical to `createComponent`. `@lit/react` already forwards
+// any prop that is not defined on the element prototype (such as `theme` on a
+// non-`ThemableMixin` element) to the DOM as an attribute, so no runtime change is
+// needed — this function exists purely to expose `theme` at the type level.
+export function createThemedComponent(options: Options): any {
+ return createComponent(options);
+}
diff --git a/scripts/generator.ts b/scripts/generator.ts
index 4cc20581..d3399bf5 100644
--- a/scripts/generator.ts
+++ b/scripts/generator.ts
@@ -25,7 +25,7 @@ import {
transform,
convertElementNameToClassName,
} from './utils/misc.js';
-import { eventSettings, genericElements, NonGenericInterface } from './utils/settings.js';
+import { eventSettings, genericElements, NonGenericInterface, themedElements } from './utils/settings.js';
// Placeholders
const CALL_EXPRESSION = '$CALL_EXPRESSION$';
@@ -319,6 +319,12 @@ function generateReactComponent({ name, js }: SchemaHTMLElement, { packageName,
namedEvents?.some(({ name }) => !eventsToRemove?.includes(name) && !eventsToBeUnknown?.includes(name)) || false;
const genericElementInfo = genericElements.get(elementName);
+ // Components that support the `theme` attribute but do not use `ThemableMixin`
+ // are generated with `createThemedComponent` so the `theme` prop is available.
+ const isThemed = themedElements.has(elementName);
+ const createFn = isThemed ? 'createThemedComponent' : 'createComponent';
+ const themeSuffix = isThemed ? ' & { theme?: string }' : '';
+
const ast = template(
`
import type { EventName } from "${LIT_REACT_PATH}";
@@ -328,7 +334,7 @@ import {
${[...new Set(genericElementInfo?.typeConstraints || [])].map((constraint) => `type ${constraint}`)}
} from "${MODULE_PATH}";
import * as React from "react";
-import { createComponent, type WebComponentProps } from "${CREATE_COMPONENT_PATH}";
+import { ${createFn}, type WebComponentProps } from "${CREATE_COMPONENT_PATH}";
export * from "${MODULE_PATH}";
@@ -338,8 +344,8 @@ export {
export type ${EVENT_MAP};
const events = ${EVENTS_DECLARATION} as ${EVENT_MAP_REF_IN_EVENTS};
-export type ${COMPONENT_NAME}Props = WebComponentProps<${COMPONENT_NAME}Element, ${EVENT_MAP}>;
-export const ${COMPONENT_NAME} = createComponent({
+export type ${COMPONENT_NAME}Props = WebComponentProps<${COMPONENT_NAME}Element, ${EVENT_MAP}>${themeSuffix};
+export const ${COMPONENT_NAME} = ${createFn}({
elementClass: ${COMPONENT_NAME}Element,
events,
react: React,
diff --git a/scripts/utils/settings.ts b/scripts/utils/settings.ts
index 8dcc5d23..b1f581ea 100644
--- a/scripts/utils/settings.ts
+++ b/scripts/utils/settings.ts
@@ -41,6 +41,11 @@ export const eventSettings = new Map([
['GridPro', { makeUnknown: ['size-changed', 'data-provider-changed'] }],
]);
+// Components that support the `theme` attribute (theme variants or propagation)
+// but do not use `ThemableMixin` / expose `ThemePropertyMixinClass` in their type.
+// These are generated with `createThemedComponent` so the `theme` prop is available.
+export const themedElements = new Set(['Switch', 'Breadcrumbs']);
+
export const elementsWithMissingEntrypoint = new Set([]);
export const elementToClassNamingConventionViolations = new Map([['vaadin-tabsheet', 'TabSheet']]);
diff --git a/test/ThemedReactWebComponent.spec.tsx b/test/ThemedReactWebComponent.spec.tsx
index b71a9678..43fdf76b 100644
--- a/test/ThemedReactWebComponent.spec.tsx
+++ b/test/ThemedReactWebComponent.spec.tsx
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-react';
import { Accordion } from '../packages/react-components/src/Accordion.js';
+import { Breadcrumbs } from '../packages/react-components/src/Breadcrumbs.js';
+import { Switch } from '../packages/react-components/src/Switch.js';
describe('ThemedReactWebComponent', () => {
it('should add a "theme" attribute', async () => {
@@ -10,4 +12,20 @@ describe('ThemedReactWebComponent', () => {
expect(element).to.have.attribute('theme', 'primary');
});
+
+ it('should add a "theme" attribute to a component without ThemableMixin (Switch)', async () => {
+ const { container } = await render();
+ const element = container.querySelector('vaadin-switch');
+ expect(element).not.to.be.undefined;
+
+ expect(element).to.have.attribute('theme', 'small');
+ });
+
+ it('should add a "theme" attribute to a component without ThemableMixin (Breadcrumbs)', async () => {
+ const { container } = await render();
+ const element = container.querySelector('vaadin-breadcrumbs');
+ expect(element).not.to.be.undefined;
+
+ expect(element).to.have.attribute('theme', 'slash');
+ });
});