diff --git a/pages/token/style-api-v2.page.tsx b/pages/token/style-api-v2.page.tsx
new file mode 100644
index 0000000000..fb20e8630a
--- /dev/null
+++ b/pages/token/style-api-v2.page.tsx
@@ -0,0 +1,50 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+import clsx from 'clsx';
+
+import { KeyValuePairs, Token } from '~components';
+
+import { SimplePage } from '../app/templates';
+
+import styles from './style-api-v2.scss';
+
+const themed = { root: styles.root, dismissButton: styles.dismiss };
+const muted = {
+ root: clsx(styles.root, styles['root-muted']),
+ dismissButton: clsx(styles.dismiss, styles['dismiss-muted']),
+};
+
+export default function () {
+ const shared = { dismissLabel: 'Remove', onDismiss: () => {} };
+ return (
+
+
+ ),
+ },
+ {
+ type: 'pair',
+ label: 'Inline variant',
+ value: ,
+ },
+ {
+ type: 'pair',
+ label: 'Disabled',
+ value: ,
+ },
+ {
+ type: 'pair',
+ label: 'Read-only',
+ value: ,
+ },
+ ]}
+ />
+
+ );
+}
diff --git a/pages/token/style-api-v2.scss b/pages/token/style-api-v2.scss
new file mode 100644
index 0000000000..c845dd3245
--- /dev/null
+++ b/pages/token/style-api-v2.scss
@@ -0,0 +1,50 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '@cloudscape-design/component-toolkit/styling' as styling;
+@use '../style-api/palette' as palette;
+
+.root {
+ @include styling.override {
+ background-color: palette.$genai-surface;
+ border-color: palette.$genai;
+ border-width: 2px;
+ border-start-start-radius: 4px;
+ border-start-end-radius: 4px;
+ border-end-start-radius: 4px;
+ border-end-end-radius: 4px;
+ padding-block: 4px;
+ padding-inline: 4px 0px;
+ }
+}
+
+.dismiss {
+ --awsui-style-focus-ring-color: #{palette.$genai};
+ --awsui-style-focus-ring-width: 2px;
+ --awsui-style-focus-ring-radius: 4px;
+
+ @include styling.override {
+ color: palette.$genai;
+
+ &:hover {
+ color: palette.$genai-hover;
+ }
+ }
+}
+
+// Disabled and read-only are applied as extra classes, because they are not element states the
+// consumer can target (double selectors bump specificity above the rules above).
+.root.root-muted {
+ @include styling.override {
+ background-color: palette.$surface-alt;
+ border-color: palette.$border-disabled;
+ }
+}
+
+.dismiss.dismiss-muted {
+ @include styling.override {
+ color: palette.$text-disabled;
+ }
+}
diff --git a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
index 0ec1499e58..cebb43af89 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
@@ -237,6 +237,42 @@ exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] =
}
`;
+exports[`Style API docs definition for token matches the snapshot: token 1`] = `
+{
+ "slots": [
+ {
+ "name": "root",
+ "properties": [
+ "background-color",
+ "border-color",
+ "border-width",
+ "border-radius",
+ "padding-block",
+ "padding-inline",
+ ],
+ "propertyDescriptions": {},
+ "tokenDescriptions": {},
+ "tokens": [],
+ },
+ {
+ "name": "dismissButton",
+ "properties": [
+ "color",
+ ],
+ "propertyDescriptions": {},
+ "tokenDescriptions": {},
+ "tokens": [
+ "focus-ring-color",
+ "focus-ring-width",
+ "focus-ring-radius",
+ "focus-ring-gutter-block",
+ "focus-ring-gutter-inline",
+ ],
+ },
+ ],
+}
+`;
+
exports[`Style API docs list of components with a Style API doc matches the snapshot 1`] = `
[
"alert",
@@ -247,5 +283,6 @@ exports[`Style API docs list of components with a Style API doc matches the snap
"link",
"radio-button",
"toggle",
+ "token",
]
`;
diff --git a/src/internal/styles/style-api.scss b/src/internal/styles/style-api.scss
index a245639edf..8e3a8eddfd 100644
--- a/src/internal/styles/style-api.scss
+++ b/src/internal/styles/style-api.scss
@@ -51,9 +51,9 @@ $_focus-ring-carriers: toolkit.resolve($focus-ring, carrier);
}
}
-@mixin docs($name, $map) {
+@mixin docs($name, $tokens: null, $properties: null, $descriptions: null) {
@if enabled() {
- @include toolkit.docs($name, $map);
+ @include toolkit.docs($name, $tokens, $properties, $descriptions);
}
}
diff --git a/src/token/__tests__/style-api.test.tsx b/src/token/__tests__/style-api.test.tsx
new file mode 100644
index 0000000000..305c2ed5c7
--- /dev/null
+++ b/src/token/__tests__/style-api.test.tsx
@@ -0,0 +1,50 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Token from '../../../lib/components/token';
+
+const slots = { root: 'a', dismissButton: 'b', unknown: 'c' };
+
+function renderToken(element: React.ReactElement) {
+ const { container } = render(element);
+ return { container, wrapper: createWrapper(container).findToken()! };
+}
+
+describe('Token Style API v2 (implicit styleClassNames)', () => {
+ test('applies styleClassNames to the root and dismissButton slots', () => {
+ const { container, wrapper } = renderToken(
+ {}} {...({ styleClassNames: slots } as any)} />
+ );
+
+ const dismissButton = wrapper.findDismiss()!.getElement();
+ expect(dismissButton).toHaveClass('b');
+ // the root slot lands on the token box, the element that carries the token's own styling
+ expect(dismissButton.parentElement).toHaveClass('a');
+ expect(container.querySelector('.c')).toBeNull();
+ });
+
+ test('applies the slots to the inline variant', () => {
+ const { wrapper } = renderToken(
+ {}}
+ {...({ styleClassNames: slots } as any)}
+ />
+ );
+ const dismissButton = wrapper.findDismiss()!.getElement();
+ expect(dismissButton).toHaveClass('b');
+ expect(dismissButton.parentElement).toHaveClass('a');
+ });
+
+ test('does not leak the styleClassNames prop to the DOM', () => {
+ const { container } = renderToken(
+ {}} {...({ styleClassNames: slots } as any)} />
+ );
+ expect(container.querySelector('[styleClassNames]')).toBeNull();
+ });
+});
diff --git a/src/token/dismiss-button.tsx b/src/token/dismiss-button.tsx
index f751760eb6..0cdfcf68a0 100644
--- a/src/token/dismiss-button.tsx
+++ b/src/token/dismiss-button.tsx
@@ -16,6 +16,7 @@ import styles from './styles.css.js';
import testUtilStyles from './test-classes/styles.css.js';
interface DismissButtonProps {
+ className?: string;
disabled?: boolean;
readOnly?: boolean;
onDismiss?: NonCancelableEventHandler;
@@ -26,7 +27,7 @@ interface DismissButtonProps {
export default forwardRef(DismissButton);
function DismissButton(
- { disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps,
+ { className, disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps,
ref: Ref
) {
const isOneTheme = isThemeActive(Theme.OneTheme);
@@ -44,7 +45,8 @@ function DismissButton(
styles['dismiss-button'],
legacyTestingStyles['dismiss-button'],
testUtilStyles['dismiss-button'],
- inline && styles['dismiss-button-inline']
+ inline && styles['dismiss-button-inline'],
+ className
)}
aria-disabled={disabled || readOnly ? true : undefined}
onClick={() => {
diff --git a/src/token/index.tsx b/src/token/index.tsx
index ffbba14e8b..3fa037676d 100644
--- a/src/token/index.tsx
+++ b/src/token/index.tsx
@@ -9,7 +9,7 @@ import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { GeneratedAnalyticsMetadataTokenComponent } from './analytics-metadata/interfaces';
import { TokenProps } from './interfaces';
-import InternalToken from './internal';
+import InternalToken, { StyleClassNames } from './internal';
import analyticsSelectors from './analytics-metadata/styles.css.js';
@@ -26,6 +26,7 @@ export default function Token(props: TokenProps) {
return (
diff --git a/src/token/internal.tsx b/src/token/internal.tsx
index 9bc1df0256..06e666968d 100644
--- a/src/token/internal.tsx
+++ b/src/token/internal.tsx
@@ -28,8 +28,15 @@ import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
import testUtilStyles from './test-classes/styles.css.js';
+// Style API v2
+export interface StyleClassNames {
+ root?: string;
+ dismissButton?: string;
+}
+
type InternalTokenProps = Omit &
InternalBaseComponentProps & {
+ styleClassNames?: StyleClassNames;
/** Token label. Required unless `__customContent` replaces the option layout entirely. */
label?: React.ReactNode;
/**
@@ -69,6 +76,7 @@ function InternalToken({
disableInnerPadding,
__tokenBoxClassName,
__customContent,
+ styleClassNames,
// Base
__internalRootRef,
@@ -187,7 +195,8 @@ function InternalToken({
readOnly && styles['token-box-readonly'],
!isInline && !onDismiss && !__customContent && styles['token-box-without-dismiss'],
disableInnerPadding && styles['disable-padding'],
- __tokenBoxClassName
+ __tokenBoxClassName,
+ styleClassNames?.root
)}
style={tokenRootStyleProps}
>
@@ -203,6 +212,7 @@ function InternalToken({
/>
{onDismiss && (