{label}
diff --git a/src/internal/components/abstract-switch/mixins.scss b/src/internal/components/abstract-switch/mixins.scss
new file mode 100644
index 0000000000..69ecd3ee0b
--- /dev/null
+++ b/src/internal/components/abstract-switch/mixins.scss
@@ -0,0 +1,23 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../../styles/style-api' as style-api;
+
+// Included by host components (checkbox, radio, toggle, tiles) on their root, to drive the
+// abstract-switch label/description styling from the host's own `--awsui-style-{host}-*` tokens.
+// `$tokens` is the host's resolved token map; only the props the host declares are propagated.
+@mixin host($tokens) {
+ @include style-api.propagate(
+ $tokens,
+ (
+ abstract-switch-label-color: label-color,
+ abstract-switch-label-color-disabled: label-color-disabled,
+ abstract-switch-label-color-readonly: label-color-readonly,
+ abstract-switch-label-spacing: label-spacing,
+ abstract-switch-transition-duration: transition-duration,
+ abstract-switch-transition-easing: transition-easing,
+ )
+ );
+}
diff --git a/src/internal/components/abstract-switch/styles.scss b/src/internal/components/abstract-switch/styles.scss
index be96eda344..b6b4acb848 100644
--- a/src/internal/components/abstract-switch/styles.scss
+++ b/src/internal/components/abstract-switch/styles.scss
@@ -13,8 +13,17 @@
display: block;
}
+/* stylelint-disable custom-property-pattern */
+
+// Label/description styling is driven by the host component (checkbox, radio, toggle, tiles) through
+// internal propagation vars set on the host root. Hosts own the public `--awsui-style-{host}-*`
+// tokens; abstract-switch only reads the resolved internal vars, falling back to design tokens.
.label {
- color: awsui.$color-text-form-default;
+ color: var(--awsui-internal-style-abstract-switch-label-color, #{awsui.$color-text-form-default});
+ @include styles.with-motion {
+ transition: color var(--awsui-internal-style-abstract-switch-transition-duration, 0s)
+ var(--awsui-internal-style-abstract-switch-transition-easing, ease);
+ }
}
.outline {
@@ -59,9 +68,12 @@
.label,
.description {
- padding-inline-start: awsui.$space-xs;
+ padding-inline-start: var(--awsui-internal-style-abstract-switch-label-spacing, #{awsui.$space-xs});
&-disabled {
- color: awsui.$color-text-control-disabled;
+ color: var(--awsui-internal-style-abstract-switch-label-color-disabled, #{awsui.$color-text-control-disabled});
+ }
+ &-readonly {
+ color: var(--awsui-internal-style-abstract-switch-label-color-readonly, #{awsui.$color-text-form-default});
}
}
diff --git a/src/internal/components/checkbox-icon/mixins.scss b/src/internal/components/checkbox-icon/mixins.scss
new file mode 100644
index 0000000000..6b514170e4
--- /dev/null
+++ b/src/internal/components/checkbox-icon/mixins.scss
@@ -0,0 +1,34 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../../styles/style-api' as style-api;
+
+// Included by host components (checkbox, and embedded checkboxes in multiselect/table selection via
+// classNames) on their root, to drive the checkbox-icon styling from the host's own
+// `--awsui-style-{host}-*` tokens. `$tokens` is the host's resolved token map; only declared props
+// are propagated, so omitted ones keep checkbox-icon's design-token fallbacks.
+@mixin host($tokens) {
+ @include style-api.propagate(
+ $tokens,
+ (
+ checkbox-icon-background: background,
+ checkbox-icon-background-default: background-default,
+ checkbox-icon-background-checked: background-checked,
+ checkbox-icon-background-disabled: background-disabled,
+ checkbox-icon-border: border-color,
+ checkbox-icon-border-default: border-color-default,
+ checkbox-icon-border-checked: border-color-checked,
+ checkbox-icon-border-disabled: border-color-disabled,
+ checkbox-icon-foreground: foreground,
+ checkbox-icon-foreground-default: foreground-default,
+ checkbox-icon-foreground-disabled: foreground-disabled,
+ checkbox-icon-foreground-readonly: foreground-readonly,
+ checkbox-icon-border-width: border-width,
+ checkbox-icon-stroke-width: stroke-width,
+ checkbox-icon-transition-duration: transition-duration,
+ checkbox-icon-transition-easing: transition-easing,
+ )
+ );
+}
diff --git a/src/internal/components/checkbox-icon/styles.scss b/src/internal/components/checkbox-icon/styles.scss
index 120f7a5bc3..dfcba98e3f 100644
--- a/src/internal/components/checkbox-icon/styles.scss
+++ b/src/internal/components/checkbox-icon/styles.scss
@@ -6,6 +6,11 @@
@use '../../styles' as styles;
@use '../../styles/tokens' as awsui;
+/* stylelint-disable custom-property-pattern */
+
+// Driven by the host (checkbox, or an embedded checkbox in multiselect/table selection) through
+// internal propagation vars set on the host root. Each state reads its own internal var, then a
+// stateless base internal var, then the design-token default.
.root {
position: absolute;
inline-size: 100%;
@@ -14,35 +19,69 @@
inset-inline-start: 0;
> .styled-box {
- fill: awsui.$color-background-control-default;
- stroke: awsui.$color-border-control-default;
- stroke-width: awsui.$border-width-field;
+ fill: var(
+ --awsui-internal-style-checkbox-icon-background-default,
+ var(--awsui-internal-style-checkbox-icon-background, #{awsui.$color-background-control-default})
+ );
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-border-default,
+ var(--awsui-internal-style-checkbox-icon-border, #{awsui.$color-border-control-default})
+ );
+ stroke-width: var(--awsui-internal-style-checkbox-icon-border-width, #{awsui.$border-width-field});
+
@include styles.with-motion {
transition:
- fill awsui.$motion-duration-transition-quick awsui.$motion-easing-transition-quick,
- stroke awsui.$motion-duration-transition-quick awsui.$motion-easing-transition-quick;
+ fill var(--awsui-internal-style-checkbox-icon-transition-duration, #{awsui.$motion-duration-transition-quick})
+ var(--awsui-internal-style-checkbox-icon-transition-easing, #{awsui.$motion-easing-transition-quick}),
+ stroke var(--awsui-internal-style-checkbox-icon-transition-duration, #{awsui.$motion-duration-transition-quick})
+ var(--awsui-internal-style-checkbox-icon-transition-easing, #{awsui.$motion-easing-transition-quick});
}
+
&-checked,
&-indeterminate {
- fill: awsui.$color-background-control-checked;
- stroke: awsui.$color-border-control-checked;
+ fill: var(
+ --awsui-internal-style-checkbox-icon-background-checked,
+ var(--awsui-internal-style-checkbox-icon-background, #{awsui.$color-background-control-checked})
+ );
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-border-checked,
+ var(--awsui-internal-style-checkbox-icon-border, #{awsui.$color-border-control-checked})
+ );
}
+
&-disabled,
&-readonly {
- fill: awsui.$color-background-control-disabled;
- stroke: awsui.$color-border-control-disabled;
+ fill: var(
+ --awsui-internal-style-checkbox-icon-background-disabled,
+ var(--awsui-internal-style-checkbox-icon-background, #{awsui.$color-background-control-disabled})
+ );
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-border-disabled,
+ var(--awsui-internal-style-checkbox-icon-border, #{awsui.$color-border-control-disabled})
+ );
}
}
> .styled-line {
- stroke: awsui.$color-foreground-control-default;
- stroke-width: 2;
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-foreground-default,
+ var(--awsui-internal-style-checkbox-icon-foreground, #{awsui.$color-foreground-control-default})
+ );
+ stroke-width: var(--awsui-internal-style-checkbox-icon-stroke-width, 2);
fill: none;
+
&-disabled {
- stroke: awsui.$color-foreground-control-disabled;
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-foreground-disabled,
+ var(--awsui-internal-style-checkbox-icon-foreground, #{awsui.$color-foreground-control-disabled})
+ );
}
+
&-readonly {
- stroke: awsui.$color-foreground-control-read-only;
+ stroke: var(
+ --awsui-internal-style-checkbox-icon-foreground-readonly,
+ var(--awsui-internal-style-checkbox-icon-foreground, #{awsui.$color-foreground-control-read-only})
+ );
}
}
}
diff --git a/src/internal/components/menu-dropdown/mixins.scss b/src/internal/components/menu-dropdown/mixins.scss
new file mode 100644
index 0000000000..374a6ecee7
--- /dev/null
+++ b/src/internal/components/menu-dropdown/mixins.scss
@@ -0,0 +1,25 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../../styles/style-api' as style-api;
+
+// Included by a menu-dropdown host (e.g. top-navigation utilities) on its root, to drive the shared
+// menu trigger and menu item styling from the host's own
+// `--awsui-style-{host}-menu-trigger-*` / `-menu-item-*` tokens.
+@mixin host($tokens) {
+ @include style-api.propagate(
+ $tokens,
+ (
+ menu-trigger-background-active: menu-trigger-background-active,
+ menu-trigger-background-hover: menu-trigger-background-hover,
+ menu-trigger-color: menu-trigger-color,
+ menu-trigger-color-active: menu-trigger-color-active,
+ menu-trigger-color-hover: menu-trigger-color-hover,
+ menu-item-color: menu-item-color,
+ menu-item-color-highlighted: menu-item-color-highlighted,
+ menu-item-background-hover: menu-item-background-hover,
+ )
+ );
+}
diff --git a/src/internal/components/menu-dropdown/styles.scss b/src/internal/components/menu-dropdown/styles.scss
index 3847f51065..819ae663ae 100644
--- a/src/internal/components/menu-dropdown/styles.scss
+++ b/src/internal/components/menu-dropdown/styles.scss
@@ -7,6 +7,8 @@
@use '../../styles/tokens' as awsui;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+/* stylelint-disable custom-property-pattern */
+
.button {
@include styles.styles-reset;
@include styles.text-wrapping;
@@ -25,21 +27,22 @@
border-block: transparent;
border-inline: transparent;
background: transparent;
- color: awsui.$color-text-interactive-default;
+ color: var(--awsui-internal-style-menu-trigger-color, #{awsui.$color-text-interactive-default});
&:hover {
- color: awsui.$color-text-interactive-hover;
+ color: var(--awsui-internal-style-menu-trigger-color-hover, #{awsui.$color-text-interactive-hover});
+ background: var(--awsui-internal-style-menu-trigger-background-hover, transparent);
text-decoration: none;
}
&:active,
&.expanded {
- background: transparent;
- color: awsui.$color-text-interactive-active;
+ background: var(--awsui-internal-style-menu-trigger-background-active, transparent);
+ color: var(--awsui-internal-style-menu-trigger-color-active, #{awsui.$color-text-interactive-active});
}
&.expanded {
- color: awsui.$color-text-accent;
+ color: var(--awsui-internal-style-menu-trigger-color-active, #{awsui.$color-text-accent});
}
&:focus {
diff --git a/src/internal/components/option/interfaces.ts b/src/internal/components/option/interfaces.ts
index 3050f4983d..3e489feb48 100644
--- a/src/internal/components/option/interfaces.ts
+++ b/src/internal/components/option/interfaces.ts
@@ -25,6 +25,10 @@ interface BaseOption {
export interface OptionDefinition extends BaseOption {
__labelPrefix?: string;
+ /**
+ * @deprecated Use the consuming component's `classNames.options` instead.
+ */
+ className?: string;
}
interface InternalOptionDefinition extends OptionDefinition {
diff --git a/src/internal/components/radio-button/mixins.scss b/src/internal/components/radio-button/mixins.scss
new file mode 100644
index 0000000000..d78c0d229e
--- /dev/null
+++ b/src/internal/components/radio-button/mixins.scss
@@ -0,0 +1,28 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../../styles/style-api' as style-api;
+
+// Included by the radio-group host on its root, to drive radio-button styling (control colors and
+// focus ring) from the host's own `--awsui-style-radio-*` tokens. Only declared props are
+// propagated, so omitted ones keep radio-button's design-token fallbacks.
+@mixin host($tokens) {
+ @include style-api.propagate(
+ $tokens,
+ (
+ radio-background-default: background-default,
+ radio-background-checked: background-checked,
+ radio-background-disabled: background-disabled,
+ radio-border-color-default: border-color-default,
+ radio-border-color-disabled: border-color-disabled,
+ radio-foreground-default: foreground-default,
+ radio-foreground-disabled: foreground-disabled,
+ radio-foreground-readonly: foreground-readonly,
+ radio-focus-ring-color: focus-ring-color,
+ radio-focus-ring-border-radius: focus-ring-border-radius,
+ radio-focus-ring-border-width: focus-ring-border-width,
+ )
+ );
+}
diff --git a/src/internal/components/radio-button/styles.scss b/src/internal/components/radio-button/styles.scss
index f40700c53e..7de7d208e8 100644
--- a/src/internal/components/radio-button/styles.scss
+++ b/src/internal/components/radio-button/styles.scss
@@ -8,6 +8,8 @@
@use '../../styles/foundation' as foundation;
@use '../../generated/custom-css-properties/index.scss' as custom-props;
+/* stylelint-disable custom-property-pattern */
+
$radio-size: awsui.$size-control;
.radio-control {
@@ -16,29 +18,38 @@ $radio-size: awsui.$size-control;
.outline {
#{custom-props.$styleFocusRingBoxShadow}: 0 0 0
- var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width)
- var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused);
+ var(
+ --awsui-internal-style-radio-focus-ring-border-width,
+ var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width)
+ )
+ var(
+ --awsui-internal-style-radio-focus-ring-color,
+ var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused)
+ );
@include styles.focus-highlight(
$gutter: 2px,
- $border-radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-circular-focus-ring),
+ $border-radius: var(
+ --awsui-internal-style-radio-focus-ring-border-radius,
+ var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-circular-focus-ring)
+ ),
$box-shadow: var(#{custom-props.$styleFocusRingBoxShadow})
);
}
.styled-circle-border {
- stroke: awsui.$color-border-control-default;
- fill: awsui.$color-background-control-default;
+ stroke: var(--awsui-internal-style-radio-border-color-default, #{awsui.$color-border-control-default});
+ fill: var(--awsui-internal-style-radio-background-default, #{awsui.$color-background-control-default});
&.styled-circle-disabled,
&.styled-circle-readonly {
- fill: awsui.$color-background-control-disabled;
- stroke: awsui.$color-background-control-disabled;
+ fill: var(--awsui-internal-style-radio-background-disabled, #{awsui.$color-background-control-disabled});
+ stroke: var(--awsui-internal-style-radio-border-color-disabled, #{awsui.$color-background-control-disabled});
}
}
.styled-circle-fill {
- stroke: awsui.$color-background-control-checked;
- fill: awsui.$color-foreground-control-default;
+ stroke: var(--awsui-internal-style-radio-background-checked, #{awsui.$color-background-control-checked});
+ fill: var(--awsui-internal-style-radio-foreground-default, #{awsui.$color-foreground-control-default});
opacity: 0;
@include styles.with-motion {
transition: opacity awsui.$motion-duration-transition-quick awsui.$motion-easing-transition-quick;
@@ -47,11 +58,11 @@ $radio-size: awsui.$size-control;
opacity: 1;
}
&.styled-circle-disabled {
- fill: awsui.$color-foreground-control-disabled;
- stroke: awsui.$color-background-control-disabled;
+ fill: var(--awsui-internal-style-radio-foreground-disabled, #{awsui.$color-foreground-control-disabled});
+ stroke: var(--awsui-internal-style-radio-background-disabled, #{awsui.$color-background-control-disabled});
}
&.styled-circle-readonly {
- fill: awsui.$color-foreground-control-read-only;
- stroke: awsui.$color-background-control-disabled;
+ fill: var(--awsui-internal-style-radio-foreground-readonly, #{awsui.$color-foreground-control-read-only});
+ stroke: var(--awsui-internal-style-radio-background-disabled, #{awsui.$color-background-control-disabled});
}
}
diff --git a/src/internal/components/selectable-item/index.tsx b/src/internal/components/selectable-item/index.tsx
index 707af46315..17fe40afe8 100644
--- a/src/internal/components/selectable-item/index.tsx
+++ b/src/internal/components/selectable-item/index.tsx
@@ -129,6 +129,7 @@ const SelectableItem = (
:
)`, where is the property
+// name WITHOUT the component prefix (e.g. `color`, `border-radius`, `header-background`). It then:
+// * `register($component, $props)` — registers each public token `--awsui-style--`
+// via @property (top level only).
+// * `carriers($component, $props)` — for non-inheriting tokens, mirrors the public token into an
+// inherited internal carrier `--awsui-internal-style--` (include on the root).
+// * `$tokens: resolve($component, $props)` + `name($tokens, )` — read sites reference a token
+// by its logical ; `name` returns the carrier when the token is non-inheriting, otherwise
+// the public token. Read sites never hardcode the `--awsui-style-`/`--awsui-internal-style-`
+// distinction, and flipping a token's inheritance updates every read automatically.
+//
+// Inheritance: `true` lets the token cascade (the common case; no carrier). `false` keeps a value
+// scoped to a single component instance (no leak into nested instances) and requires `carriers`.
+//
+// `syntax: '*'` is required: it keeps the property's initial value as the guaranteed-invalid value,
+// so an unset token falls through to each read site's own `var(..., )` fallback.
+
+// Convenience: build a `(: )` map from a list of props that all share inheritance.
+@function uniform($props, $inherits) {
+ $result: ();
+ @each $prop in $props {
+ $result: map.set($result, $prop, $inherits);
+ }
+ @return $result;
+}
+
+// Build `(: )`, resolving to the internal carrier for non-inheriting
+// tokens and the public token otherwise.
+@function resolve($component, $props) {
+ $tokens: ();
+ @each $prop, $inherits in $props {
+ $public: string.unquote('--awsui-style-#{$component}-#{$prop}');
+ $carrier: string.unquote('--awsui-internal-style-#{$component}-#{$prop}');
+ $tokens: map.set($tokens, $prop, if($inherits, $public, $carrier));
+ }
+ @return $tokens;
+}
+
+// The custom-property name to read for `$prop` (carrier or public, as resolved). Use as
+// `var(#{style-api.name($tokens, )}, )`. Errors at compile time on an unknown prop
+// (typo or a read of an undeclared token), so it can never silently emit an invalid `var(, …)`.
+@function name($tokens, $prop) {
+ @if not map.has-key($tokens, $prop) {
+ @error 'Unknown style token "#{$prop}". Declared tokens: #{map.keys($tokens)}.';
+ }
+ @return map.get($tokens, $prop);
+}
+
+// Whether a component declares `$prop`. Use to optionally propagate a token into an internal
+// component (a host that doesn't expose a given token leaves the internal's design-token fallback).
+@function has($tokens, $prop) {
+ @return map.has-key($tokens, $prop);
+}
+
+@mixin register($component, $props) {
+ @each $prop, $inherits in $props {
+ @property --awsui-style-#{$component}-#{$prop} {
+ syntax: '*';
+ inherits: $inherits;
+ }
+ }
+}
+
+@mixin carriers($component, $props) {
+ @each $prop, $inherits in $props {
+ @if not $inherits {
+ --awsui-internal-style-#{$component}-#{$prop}: var(--awsui-style-#{$component}-#{$prop});
+ }
+ }
+}
+
+// Propagate a host's tokens into the internal vars an internal component reads. `$map` is
+// `(: )`; only props the host declares are emitted, so any the host
+// omits keep the internal component's own design-token fallback. Include on the host root (the
+// internal vars inherit to the internal component's elements). Used by internal components' `host()`
+// mixins so each var->prop mapping lives in exactly one place.
+@mixin propagate($tokens, $map) {
+ @each $internal, $prop in $map {
+ @if map.has-key($tokens, $prop) {
+ --awsui-internal-style-#{$internal}: var(#{map.get($tokens, $prop)});
+ }
+ }
+}
diff --git a/src/item-card/interfaces.ts b/src/item-card/interfaces.ts
index 4db3f3b3c9..2c01df1f3c 100644
--- a/src/item-card/interfaces.ts
+++ b/src/item-card/interfaces.ts
@@ -75,6 +75,14 @@ export interface ItemCardProps extends BaseComponentProps {
*/
style?: ItemCardProps.Style;
+ /**
+ * An object that maps the item card's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The item card's root element.
+ * @awsuiSystem core
+ */
+ classNames?: ItemCardProps.ClassNames;
+
/**
* Attributes to add to the native root element.
* Some attributes will be automatically combined with internal attribute values:
@@ -91,6 +99,10 @@ export interface ItemCardProps extends BaseComponentProps {
export namespace ItemCardProps {
export type Variant = 'embedded' | 'default';
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface Style {
root?: {
background?: string;
diff --git a/src/item-card/internal.tsx b/src/item-card/internal.tsx
index 0252a9dc37..7fd77a195a 100644
--- a/src/item-card/internal.tsx
+++ b/src/item-card/internal.tsx
@@ -18,6 +18,7 @@ export default function InternalItemCard({
highlighted,
children,
className,
+ classNames,
header,
description,
footer,
@@ -43,6 +44,7 @@ export default function InternalItemCard({
{
className: clsx(
className,
+ classNames?.root,
styles.root,
highlighted && styles.highlighted,
fullHeight && styles['full-height'],
diff --git a/src/item-card/styles.scss b/src/item-card/styles.scss
index f9e34660aa..daa880cb64 100644
--- a/src/item-card/styles.scss
+++ b/src/item-card/styles.scss
@@ -6,10 +6,32 @@
@use 'sass:map';
@use 'sass:math';
+/* stylelint-disable custom-property-pattern */
+
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@use './motion';
+@use '../internal/styles/style-api' as style-api;
+
+// Register public Style API tokens as @property.
+$component: 'item-card';
+$props: style-api.uniform(
+ (
+ background-default,
+ border-color-default,
+ border-radius,
+ border-width-default,
+ box-shadow-default,
+ footer-divider-border-color,
+ footer-divider-border-width,
+ footer-divider-style
+ ),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
+
+@include style-api.register($component, $props);
// Variant configuration maps — adjust values here for easy customization
$variant-border-radius: (
@@ -32,10 +54,22 @@ $action-padding-horizontal: awsui.$space-xxs;
@mixin apply-border-radius($variant: 'default') {
$radius: map.get($variant-border-radius, $variant);
- border-start-start-radius: var(#{custom-props.$styleItemCardBorderRadius}, $radius);
- border-start-end-radius: var(#{custom-props.$styleItemCardBorderRadius}, $radius);
- border-end-start-radius: var(#{custom-props.$styleItemCardBorderRadius}, $radius);
- border-end-end-radius: var(#{custom-props.$styleItemCardBorderRadius}, $radius);
+ border-start-start-radius: var(
+ #{style-api.name($tokens, border-radius)},
+ var(#{custom-props.$styleItemCardBorderRadius}, $radius)
+ );
+ border-start-end-radius: var(
+ #{style-api.name($tokens, border-radius)},
+ var(#{custom-props.$styleItemCardBorderRadius}, $radius)
+ );
+ border-end-start-radius: var(
+ #{style-api.name($tokens, border-radius)},
+ var(#{custom-props.$styleItemCardBorderRadius}, $radius)
+ );
+ border-end-end-radius: var(
+ #{style-api.name($tokens, border-radius)},
+ var(#{custom-props.$styleItemCardBorderRadius}, $radius)
+ );
}
.header {
@@ -52,6 +86,10 @@ $action-padding-horizontal: awsui.$space-xxs;
}
.footer {
+ border-block-start-style: var(#{style-api.name($tokens, footer-divider-style)}, none);
+ border-block-start-color: var(#{style-api.name($tokens, footer-divider-border-color)}, transparent);
+ border-block-start-width: var(#{style-api.name($tokens, footer-divider-border-width)}, 0);
+
&:first-child {
margin-block-start: auto;
}
@@ -61,18 +99,38 @@ $action-padding-horizontal: awsui.$space-xxs;
@include styles.styles-reset();
box-sizing: border-box;
position: relative;
- background: var(#{custom-props.$styleItemCardBackgroundDefault}, awsui.$color-background-item-card);
+ background: var(
+ #{style-api.name($tokens, background-default)},
+ var(#{custom-props.$styleItemCardBackgroundDefault}, awsui.$color-background-item-card)
+ );
min-inline-size: 0;
- box-shadow: var(#{custom-props.$styleItemCardBoxShadowDefault}, awsui.$shadow-item-card);
+ box-shadow: var(
+ #{style-api.name($tokens, box-shadow-default)},
+ var(#{custom-props.$styleItemCardBoxShadowDefault}, awsui.$shadow-item-card)
+ );
&:before {
@include styles.base-pseudo-element;
box-shadow: none;
border-color: transparent;
- border-block: solid var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-width-item-card)
- var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-item-card);
- border-inline: solid var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-width-item-card)
- var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-item-card);
+ border-block: solid
+ var(
+ #{style-api.name($tokens, border-width-default)},
+ var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-width-item-card)
+ )
+ var(
+ #{style-api.name($tokens, border-color-default)},
+ var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-item-card)
+ );
+ border-inline: solid
+ var(
+ #{style-api.name($tokens, border-width-default)},
+ var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-width-item-card)
+ )
+ var(
+ #{style-api.name($tokens, border-color-default)},
+ var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-item-card)
+ );
}
&::after {
@@ -80,8 +138,15 @@ $action-padding-horizontal: awsui.$space-xxs;
}
&:not(.refresh)::before {
- border-block-start: solid var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-container-top-width)
- var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-container-top);
+ border-block-start: solid
+ var(
+ #{style-api.name($tokens, border-width-default)},
+ var(#{custom-props.$styleItemCardBorderWidthDefault}, awsui.$border-container-top-width)
+ )
+ var(
+ #{style-api.name($tokens, border-color-default)},
+ var(#{custom-props.$styleItemCardBorderColorDefault}, awsui.$color-border-container-top)
+ );
}
&.highlighted {
diff --git a/src/link/interfaces.ts b/src/link/interfaces.ts
index e55c6d97e6..c8d095418d 100644
--- a/src/link/interfaces.ts
+++ b/src/link/interfaces.ts
@@ -118,6 +118,14 @@ export interface LinkProps extends BaseComponentProps {
*/
style?: LinkProps.Style;
+ /**
+ * An object that maps the link's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The link's root element.
+ * @awsuiSystem core
+ */
+ classNames?: LinkProps.ClassNames;
+
/**
* Attributes to add to the native element.
* Some attributes will be automatically combined with internal attribute values:
@@ -169,4 +177,8 @@ export namespace LinkProps {
};
};
}
+
+ export interface ClassNames {
+ root?: string;
+ }
}
diff --git a/src/link/internal.tsx b/src/link/internal.tsx
index e996af82d0..eeb972049f 100644
--- a/src/link/internal.tsx
+++ b/src/link/internal.tsx
@@ -55,6 +55,7 @@ const InternalLink = React.forwardRef(
nativeAttributes,
__internalRootRef,
style,
+ classNames,
...props
}: InternalLinkProps,
ref: React.Ref
@@ -172,6 +173,7 @@ const InternalLink = React.forwardRef(
className: clsx(
styles.link,
baseProps.className,
+ classNames?.root,
applyButtonStyles ? styles.button : null,
styles[getVariantStyle(variant)],
styles[getFontSizeStyle(variant, fontSize)],
diff --git a/src/link/style-tokens.scss b/src/link/style-tokens.scss
new file mode 100644
index 0000000000..704385531e
--- /dev/null
+++ b/src/link/style-tokens.scss
@@ -0,0 +1,25 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../internal/styles/style-api' as style-api;
+
+// Link's public Style API tokens. The link visual styling lives in the shared
+// internal/styles/links.scss mixin (used for standalone links and links embedded in other
+// components), which reads these via style-api.name($tokens, ...).
+$component: 'link';
+$props: style-api.uniform(
+ (
+ color-default,
+ color-hover,
+ color-active,
+ font-weight,
+ text-decoration,
+ focus-ring-color,
+ focus-ring-border-radius,
+ focus-ring-border-width
+ ),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
diff --git a/src/link/styles.scss b/src/link/styles.scss
index 5a327ce573..05183c8c1e 100644
--- a/src/link/styles.scss
+++ b/src/link/styles.scss
@@ -10,6 +10,13 @@
@use './constants' as constants;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@use '../internal/styles/foundation' as foundation;
+@use '../internal/styles/style-api' as style-api;
+@use './style-tokens' as style-tokens;
+
+/* stylelint-disable custom-property-pattern */
+
+// Register link's public Style API tokens (declared in ./style-tokens; read in the shared links mixin).
+@include style-api.register(style-tokens.$component, style-tokens.$props);
.link {
@include styles.styles-reset;
@@ -17,8 +24,14 @@
white-space: inherit;
#{custom-props.$styleFocusRingBoxShadow}: 0 0 0
- var(#{custom-props.$styleFocusRingBorderWidth}, awsui.$border-link-focus-ring-shadow-spread)
- var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused);
+ var(
+ --awsui-style-link-focus-ring-border-width,
+ var(#{custom-props.$styleFocusRingBorderWidth}, awsui.$border-link-focus-ring-shadow-spread)
+ )
+ var(
+ --awsui-style-link-focus-ring-color,
+ var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused)
+ );
@include styles.link-default;
@@ -53,8 +66,14 @@
@include focus-visible.when-visible {
@include styles.link-focus(
- $border-color: var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused),
- $border-radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring),
+ $border-color: var(
+ --awsui-style-link-focus-ring-color,
+ var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused)
+ ),
+ $border-radius: var(
+ --awsui-style-link-focus-ring-border-radius,
+ var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring)
+ ),
$box-shadow: var(#{custom-props.$styleFocusRingBoxShadow})
);
}
diff --git a/src/modal/interfaces.ts b/src/modal/interfaces.ts
index 44b216e321..2908929da9 100644
--- a/src/modal/interfaces.ts
+++ b/src/modal/interfaces.ts
@@ -104,12 +104,24 @@ export interface ModalProps extends BaseComponentProps, BaseModalProps {
* @analytics
*/
analyticsMetadata?: ModalProps.AnalyticsMetadata;
+
+ /**
+ * An object that maps the modal's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The modal's root element.
+ * @awsuiSystem core
+ */
+ classNames?: ModalProps.ClassNames;
}
export namespace ModalProps {
export type Size = 'small' | 'medium' | 'large' | 'x-large' | 'xx-large' | 'max';
export type Position = 'center' | 'top';
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface DismissDetail {
reason: string;
}
diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx
index 16761199a1..086c3ae86a 100644
--- a/src/modal/internal.tsx
+++ b/src/modal/internal.tsx
@@ -108,6 +108,7 @@ function PortaledModal({
__subStepRef,
__subStepFunnelProps,
referrerId,
+ classNames,
...rest
}: PortaledModalProps) {
const instanceUniqueId = useUniqueId();
@@ -246,6 +247,7 @@ function PortaledModal({
styles.root,
{ [styles.hidden]: !visible },
baseProps.className,
+ classNames?.root,
isRefresh && styles.refresh
)}
role="dialog"
diff --git a/src/modal/styles.scss b/src/modal/styles.scss
index 1c26f4b0be..04c06bd2f2 100644
--- a/src/modal/styles.scss
+++ b/src/modal/styles.scss
@@ -8,12 +8,22 @@
@use '../container/shared' as container;
@use './motion';
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
+@use '../internal/styles/style-api' as style-api;
+
+/* stylelint-disable custom-property-pattern */
+
+// Register public Style API tokens as @property.
+$component: 'modal';
+$props: style-api.uniform((background, border-radius, header-background, overlay-background), $inherits: true);
+$tokens: style-api.resolve($component, $props);
+
+@include style-api.register($component, $props);
$modal-z-index: 5000;
.root {
@include styles.styles-reset;
- background-color: awsui.$color-background-modal-overlay;
+ background-color: var(#{style-api.name($tokens, overlay-background)}, #{awsui.$color-background-modal-overlay});
display: flex;
align-items: center;
@@ -102,13 +112,13 @@ $modal-z-index: 5000;
.container {
@include styles.styles-reset;
display: block;
- background-color: awsui.$color-background-container-content;
+ background-color: var(#{style-api.name($tokens, background)}, #{awsui.$color-background-container-content});
word-wrap: break-word;
border-block-start: awsui.$border-container-top-width solid awsui.$color-border-container-top;
- border-start-start-radius: awsui.$border-radius-container;
- border-start-end-radius: awsui.$border-radius-container;
- border-end-start-radius: awsui.$border-radius-container;
- border-end-end-radius: awsui.$border-radius-container;
+ border-start-start-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
+ border-start-end-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
+ border-end-start-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
+ border-end-end-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
box-shadow: awsui.$shadow-modal;
&.custom-height-container {
@@ -139,10 +149,10 @@ $modal-z-index: 5000;
padding-block-start: awsui.$space-container-header-top;
padding-block-end: awsui.$space-container-header-bottom;
padding-inline: awsui.$space-modal-horizontal;
- background-color: awsui.$color-background-container-header;
+ background-color: var(#{style-api.name($tokens, header-background)}, #{awsui.$color-background-container-header});
border-block-end: 1px solid awsui.$color-border-container-divider;
- border-start-start-radius: awsui.$border-radius-container;
- border-start-end-radius: awsui.$border-radius-container;
+ border-start-start-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
+ border-start-end-radius: var(#{style-api.name($tokens, border-radius)}, #{awsui.$border-radius-container});
border-end-start-radius: 0;
border-end-end-radius: 0;
}
diff --git a/src/multiselect/interfaces.ts b/src/multiselect/interfaces.ts
index e031bfa715..2fa419aa29 100644
--- a/src/multiselect/interfaces.ts
+++ b/src/multiselect/interfaces.ts
@@ -80,6 +80,16 @@ export interface MultiselectProps extends BaseSelectProps {
* Specifies a render function to render custom options in the dropdown menu.
*/
renderOption?: MultiselectProps.MultiselectOptionItemRenderer;
+
+ /**
+ * An object that maps the multiselect's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The multiselect's root element.
+ * * `options` - Each option item. A string is applied to all; a function receiving
+ * `{ option }` enables per-option customization.
+ * @awsuiSystem core
+ */
+ classNames?: MultiselectProps.ClassNames;
}
export namespace MultiselectProps {
@@ -121,6 +131,11 @@ export namespace MultiselectProps {
filterText?: string;
}) => ReactNode | null;
+ export interface ClassNames {
+ root?: string;
+ options?: string | ((args: { option: MultiselectProps.Option }) => string | undefined);
+ }
+
export type DeselectAriaLabelFunction = (option: Option) => string;
export type TriggerVariant = 'placeholder' | 'tokens';
diff --git a/src/multiselect/internal.tsx b/src/multiselect/internal.tsx
index 28b50ce0e9..d8b99eaca6 100644
--- a/src/multiselect/internal.tsx
+++ b/src/multiselect/internal.tsx
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React, { useRef, useState } from 'react';
+import React, { useMemo, useRef, useState } from 'react';
import clsx from 'clsx';
import { useResizeObserver, useUniqueId } from '@cloudscape-design/component-toolkit/internal';
@@ -10,6 +10,7 @@ import { useInternalI18n } from '../i18n/context';
import { getBaseProps } from '../internal/base-component';
import { getBreakpointValue } from '../internal/breakpoints';
import DropdownFooter from '../internal/components/dropdown-footer/index.js';
+import { isGroup } from '../internal/components/option/utils/filter-options';
import ScreenreaderOnly from '../internal/components/screenreader-only';
import { useFormFieldContext } from '../internal/context/form-field-context';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component/index.js';
@@ -63,6 +64,7 @@ const InternalMultiselect = React.forwardRef(
autoFocus,
enableSelectAll,
renderOption,
+ classNames,
...restProps
}: InternalMultiselectProps,
externalRef: React.Ref
@@ -71,6 +73,19 @@ const InternalMultiselect = React.forwardRef(
const formFieldContext = useFormFieldContext(restProps);
const i18n = useInternalI18n('multiselect');
+ const resolvedOptions = useMemo(() => {
+ if (!classNames?.options) {
+ return options;
+ }
+ const resolve = (option: MultiselectProps.Option): string | undefined =>
+ typeof classNames.options === 'function' ? classNames.options({ option }) : classNames.options;
+ return options.map(entry =>
+ isGroup(entry)
+ ? { ...entry, options: entry.options.map(opt => ({ ...opt, className: resolve(opt) ?? opt.className })) }
+ : { ...entry, className: resolve(entry) ?? entry.className }
+ );
+ }, [options, classNames]);
+
const selfControlId = useUniqueId('trigger');
const controlId = formFieldContext.controlId ?? selfControlId;
const ariaLabelId = useUniqueId('multiselect-ariaLabel-');
@@ -78,7 +93,7 @@ const InternalMultiselect = React.forwardRef(
const [filteringValue, setFilteringValue] = useState('');
const multiselectProps = useMultiselect({
- options,
+ options: resolvedOptions,
selectedOptions,
filteringType,
disabled,
@@ -167,7 +182,7 @@ const InternalMultiselect = React.forwardRef(
diff --git a/src/progress-bar/interfaces.ts b/src/progress-bar/interfaces.ts
index 72d1abf549..a66b892ba1 100644
--- a/src/progress-bar/interfaces.ts
+++ b/src/progress-bar/interfaces.ts
@@ -89,12 +89,24 @@ export interface ProgressBarProps extends BaseComponentProps {
* @awsuiSystem core
*/
style?: ProgressBarProps.Style;
+
+ /**
+ * An object that maps the progress bar's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The progress bar's root element.
+ * @awsuiSystem core
+ */
+ classNames?: ProgressBarProps.ClassNames;
}
export namespace ProgressBarProps {
export type Status = 'in-progress' | 'success' | 'error';
export type Variant = 'standalone' | 'flash' | 'key-value';
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface Style {
progressBar?: {
backgroundColor?: string;
diff --git a/src/progress-bar/styles.scss b/src/progress-bar/styles.scss
index bbb99c06ae..82babf801d 100644
--- a/src/progress-bar/styles.scss
+++ b/src/progress-bar/styles.scss
@@ -2,21 +2,39 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
+/* stylelint-disable custom-property-pattern */
@use '../internal/styles/tokens' as awsui;
@use '../internal/styles' as styles;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
+@use '../internal/styles/style-api' as style-api;
@use './motion';
-$progress-height: var(#{custom-props.$progressBarHeight}, 0.4 * styles.$base-size);
-$progress-border-radius: var(#{custom-props.$progressBarBorderRadius}, 10px);
+// Register public Style API tokens as @property.
+$component: 'progress-bar';
+$props: style-api.uniform(
+ (border-radius, height, percentage-color, percentage-font-size, percentage-font-weight, track-color, value-color),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
+
+@include style-api.register($component, $props);
+
+$progress-height: var(
+ #{style-api.name($tokens, height)},
+ var(#{custom-props.$progressBarHeight}, 0.4 * styles.$base-size)
+);
+$progress-border-radius: var(
+ #{style-api.name($tokens, border-radius)},
+ var(#{custom-props.$progressBarBorderRadius}, 10px)
+);
$progress-background-color: var(
- #{custom-props.$progressBarBackgroundColor},
- awsui.$color-background-progress-bar-default
+ #{style-api.name($tokens, track-color)},
+ var(#{custom-props.$progressBarBackgroundColor}, awsui.$color-background-progress-bar-default)
);
$progress-value-background-color: var(
- #{custom-props.$progressValueBackgroundColor},
- awsui.$color-background-progress-bar-value-default
+ #{style-api.name($tokens, value-color)},
+ var(#{custom-props.$progressValueBackgroundColor}, awsui.$color-background-progress-bar-value-default)
);
.root {
@@ -80,6 +98,9 @@ $progress-value-background-color: var(
.percentage {
/* used in test-utils */
+ color: var(#{style-api.name($tokens, percentage-color)}, inherit);
+ font-size: var(#{style-api.name($tokens, percentage-font-size)}, inherit);
+ font-weight: var(#{style-api.name($tokens, percentage-font-weight)}, inherit);
}
@mixin general-progress-background-style {
diff --git a/src/prompt-input/interfaces.ts b/src/prompt-input/interfaces.ts
index 8dbc44fe52..ac7f57ae34 100644
--- a/src/prompt-input/interfaces.ts
+++ b/src/prompt-input/interfaces.ts
@@ -311,9 +311,21 @@ export interface PromptInputProps
* @awsuiSystem core
*/
style?: PromptInputProps.Style;
+
+ /**
+ * An object that maps the prompt input's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The prompt input's root element.
+ * @awsuiSystem core
+ */
+ classNames?: PromptInputProps.ClassNames;
}
export namespace PromptInputProps {
+ export interface ClassNames {
+ root?: string;
+ }
+
export type KeyDetail = BaseKeyDetail;
export interface I18nStrings {
diff --git a/src/prompt-input/internal.tsx b/src/prompt-input/internal.tsx
index 2e4ec2a0ee..900aa6826d 100644
--- a/src/prompt-input/internal.tsx
+++ b/src/prompt-input/internal.tsx
@@ -80,6 +80,7 @@ const InternalPromptInput = React.forwardRef(
onTriggerDetected,
i18nStrings,
__internalRootRef,
+ classNames,
...rest
}: InternalPromptInputProps,
ref: Ref
@@ -384,7 +385,7 @@ const InternalPromptInput = React.forwardRef(
+
{!renderResult && hasCheckbox && (
diff --git a/src/slider/interfaces.ts b/src/slider/interfaces.ts
index c2e328bb37..1bd4d31d95 100644
--- a/src/slider/interfaces.ts
+++ b/src/slider/interfaces.ts
@@ -89,9 +89,21 @@ export interface SliderProps extends BaseComponentProps, FormFieldValidationCont
* @awsuiSystem core
*/
style?: SliderProps.Style;
+
+ /**
+ * An object that maps the slider's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The slider's root element.
+ * @awsuiSystem core
+ */
+ classNames?: SliderProps.ClassNames;
}
export namespace SliderProps {
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface ChangeDetail {
value: number;
}
diff --git a/src/slider/internal.tsx b/src/slider/internal.tsx
index e1de93e7e9..5ed86dc42d 100644
--- a/src/slider/internal.tsx
+++ b/src/slider/internal.tsx
@@ -47,6 +47,7 @@ export default function InternalSlider({
valueFormatter,
i18nStrings,
style,
+ classNames,
__internalRootRef,
...rest
}: InternalSliderProps) {
@@ -132,7 +133,7 @@ export default function InternalSlider({
.header-cell-content:hover,
&.header-cell-sorted > .header-cell-content {
- color: awsui.$color-text-interactive-active;
+ color: var(#{style-api.name(style-tokens.$tokens, header-color)}, #{awsui.$color-text-interactive-active});
& > .sorting-icon {
- color: awsui.$color-text-interactive-active;
+ color: var(#{style-api.name(style-tokens.$tokens, sorting-icon-color)}, #{awsui.$color-text-interactive-active});
}
}
}
diff --git a/src/table/interfaces.tsx b/src/table/interfaces.tsx
index 1628cf5492..d3a7b698e2 100644
--- a/src/table/interfaces.tsx
+++ b/src/table/interfaces.tsx
@@ -144,6 +144,24 @@ export interface TableProps
extends BaseComponentProps {
*/
selectedItems?: ReadonlyArray;
+ /**
+ * Specifies a class name applied to each selection cell.
+ * Can be a string (applied to all) or a function receiving `{ item }` for row-level
+ * customization. When called for the header "select all" control, `item` is `undefined`.
+ * @deprecated Use `classNames.selection` instead.
+ */
+ selectionClassName?: string | ((props: { item: T | undefined }) => string);
+
+ /**
+ * An object that maps the table's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The table's root element.
+ * * `selection` - Each selection cell. A string is applied to all; a function receiving
+ * `{ item }` enables row-level customization (`item` is `undefined` for the header "select all" control).
+ * @awsuiSystem core
+ */
+ classNames?: TableProps.ClassNames;
+
/**
* Use this slot to add filtering controls to the table.
*/
@@ -432,6 +450,11 @@ export interface TableProps extends BaseComponentProps {
}
export namespace TableProps {
+ export interface ClassNames {
+ root?: string;
+ selection?: string | ((props: { item: T | undefined }) => string);
+ }
+
export interface AnalyticsMetadata {
instanceIdentifier?: string;
flowType?: 'view-resource';
diff --git a/src/table/internal.tsx b/src/table/internal.tsx
index cf2e4db610..4ea2588af0 100644
--- a/src/table/internal.tsx
+++ b/src/table/internal.tsx
@@ -113,6 +113,8 @@ const InternalTable = React.forwardRef(
selectionType: externalSelectionType,
selectedItems,
isItemDisabled,
+ selectionClassName: selectionClassNameProp,
+ classNames,
ariaLabels,
onSelectionChange,
onSortingChange,
@@ -159,6 +161,9 @@ const InternalTable = React.forwardRef(
const baseProps = getBaseProps(rest);
+ // `classNames.selection` supersedes the deprecated `selectionClassName` prop.
+ const selectionClassName = classNames?.selection ?? selectionClassNameProp;
+
const prevStickyHeader = usePrevious(stickyHeader);
if (prevStickyHeader !== undefined && !!stickyHeader !== !!prevStickyHeader) {
warnOnce(
@@ -393,6 +398,8 @@ const InternalTable = React.forwardRef(
const theadProps: TheadProps = {
selectionType,
getSelectAllProps: selection.getSelectAllProps,
+ selectionClassName:
+ typeof selectionClassName === 'function' ? selectionClassName({ item: undefined }) : selectionClassName,
columnDefinitions: visibleColumnDefinitions,
variant: computedVariant,
tableVariant: computedVariant,
@@ -465,7 +472,7 @@ const InternalTable = React.forwardRef(
{...baseProps}
{...tableInteractionAttributes}
__internalRootRef={__internalRootRef}
- className={clsx(baseProps.className, styles.root)}
+ className={clsx(baseProps.className, classNames?.root, styles.root)}
__funnelSubStepProps={__funnelSubStepProps}
__fullPage={variant === 'full-page'}
header={
@@ -647,6 +654,10 @@ const InternalTable = React.forwardRef(
onFocusUp: moveFocusUp,
rowIndex,
itemKey: rowId,
+ selectionClassName:
+ typeof selectionClassName === 'function'
+ ? selectionClassName({ item: row.item })
+ : selectionClassName,
}}
verticalAlign={cellVerticalAlign}
tableVariant={computedVariant}
@@ -744,7 +755,17 @@ const InternalTable = React.forwardRef(
columnId={selectionColumnId}
verticalAlign={cellVerticalAlign}
tableVariant={computedVariant}
- selectionControlProps={selectionType === 'group' ? loaderSelectionProps : undefined}
+ selectionControlProps={
+ selectionType === 'group' && loaderSelectionProps
+ ? {
+ ...loaderSelectionProps,
+ selectionClassName:
+ typeof selectionClassName === 'function'
+ ? selectionClassName({ item: row.item ?? undefined })
+ : selectionClassName,
+ }
+ : undefined
+ }
isSelected={selectionType === 'group' && !!loaderSelectionProps?.checked}
/>
) : null}
diff --git a/src/table/selection/selection-cell.tsx b/src/table/selection/selection-cell.tsx
index f2b894af81..4ca6e8648e 100644
--- a/src/table/selection/selection-cell.tsx
+++ b/src/table/selection/selection-cell.tsx
@@ -18,6 +18,7 @@ interface TableHeaderSelectionCellProps extends Omit ItemSelectionProps;
onFocusMove: ((sourceElement: HTMLElement, fromIndex: number, direction: -1 | 1) => void) | undefined;
+ selectionClassName?: string;
}
interface TableBodySelectionCellProps
@@ -30,6 +31,7 @@ export function TableHeaderSelectionCell({
singleSelectionHeaderAriaLabel,
getSelectAllProps,
onFocusMove,
+ selectionClassName,
...props
}: TableHeaderSelectionCellProps) {
const selectAllProps = getSelectAllProps ? getSelectAllProps() : undefined;
@@ -50,6 +52,7 @@ export function TableHeaderSelectionCell({
onFocusMove!(event.target as HTMLElement, -1, +1);
}}
focusedComponent={focusedComponent}
+ selectionClassName={selectionClassName}
{...selectAllProps}
{...(props.sticky ? { tabIndex: -1 } : {})}
/>
diff --git a/src/table/selection/selection-control.tsx b/src/table/selection/selection-control.tsx
index 7d6b7458f5..f91c86b1f4 100644
--- a/src/table/selection/selection-control.tsx
+++ b/src/table/selection/selection-control.tsx
@@ -23,6 +23,7 @@ export interface SelectionControlProps extends ItemSelectionProps {
rowIndex?: number;
itemKey?: string;
verticalAlign?: 'middle' | 'top';
+ selectionClassName?: string;
}
export function SelectionControl({
@@ -38,6 +39,7 @@ export function SelectionControl({
rowIndex,
itemKey,
verticalAlign = 'middle',
+ selectionClassName,
onChange,
...sharedProps
}: SelectionControlProps) {
@@ -123,7 +125,7 @@ export function SelectionControl({
onMouseUp={setShiftState}
onClick={handleClick}
htmlFor={controlId}
- className={clsx(styles.label, styles.root, verticalAlign === 'top' && styles['label-top'])}
+ className={clsx(styles.label, styles.root, verticalAlign === 'top' && styles['label-top'], selectionClassName)}
aria-label={ariaLabel}
title={ariaLabel}
{...(rowIndex !== undefined && !sharedProps.disabled
diff --git a/src/table/style-tokens.scss b/src/table/style-tokens.scss
new file mode 100644
index 0000000000..30f8eba5d3
--- /dev/null
+++ b/src/table/style-tokens.scss
@@ -0,0 +1,14 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../internal/styles/style-api' as style-api;
+
+// Table's public Style API tokens (read in styles.scss and the body-cell/header-cell sub-files).
+$component: 'table';
+$props: style-api.uniform(
+ (border-color, header-background, header-color, selected-background, selected-border-color, sorting-icon-color),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
diff --git a/src/table/styles.scss b/src/table/styles.scss
index 186090c9bd..e936875df3 100644
--- a/src/table/styles.scss
+++ b/src/table/styles.scss
@@ -5,9 +5,16 @@
@use '../internal/styles/index' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use './style-tokens' as style-tokens;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use '../container/shared' as container;
+/* stylelint-disable custom-property-pattern */
+
+// Register table's public Style API tokens as inheriting @property custom properties.
+@include style-api.register(style-tokens.$component, style-tokens.$props);
+
.root {
@include styles.default-text-style;
inline-size: 100%;
@@ -153,7 +160,7 @@ filter search icon.
border-start-end-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
- background: awsui.$color-background-table-header;
+ background: var(#{style-api.name(style-tokens.$tokens, header-background)}, #{awsui.$color-background-table-header});
&.variant-full-page {
background: awsui.$color-background-layout-main;
}
diff --git a/src/table/thead.tsx b/src/table/thead.tsx
index 10024c014e..52a7e8290d 100644
--- a/src/table/thead.tsx
+++ b/src/table/thead.tsx
@@ -19,6 +19,8 @@ import styles from './styles.css.js';
export interface TheadProps {
selectionType: undefined | InternalSelectionType;
+ getSelectAllProps?: () => ItemSelectionProps;
+ selectionClassName?: string;
columnDefinitions: ReadonlyArray>;
sortingColumn: TableProps.SortingColumn | undefined;
sortingDescending: boolean | undefined;
@@ -27,7 +29,6 @@ export interface TheadProps {
tableVariant?: TableProps.Variant;
wrapLines: boolean | undefined;
resizableColumns: boolean | undefined;
- getSelectAllProps?: () => ItemSelectionProps;
onFocusMove: ((sourceElement: HTMLElement, fromIndex: number, direction: -1 | 1) => void) | undefined;
onResizeFinish: (newWidths: Map) => void;
onSortingChange: NonCancelableEventHandler> | undefined;
@@ -52,6 +53,7 @@ const Thead = React.forwardRef(
{
selectionType,
getSelectAllProps,
+ selectionClassName,
columnDefinitions,
sortingColumn,
sortingDisabled,
@@ -115,6 +117,7 @@ const Thead = React.forwardRef(
getSelectAllProps={getSelectAllProps}
onFocusMove={onFocusMove}
singleSelectionHeaderAriaLabel={singleSelectionHeaderAriaLabel}
+ selectionClassName={selectionClassName}
/>
) : null}
diff --git a/src/tabs/index.tsx b/src/tabs/index.tsx
index ad54fcde07..b9143398f2 100644
--- a/src/tabs/index.tsx
+++ b/src/tabs/index.tsx
@@ -56,6 +56,7 @@ export default function Tabs({
keyboardActivationMode = 'automatic',
actions,
style,
+ classNames,
...rest
}: TabsProps) {
for (const tab of tabs) {
@@ -165,7 +166,7 @@ export default function Tabs({
header={header}
disableHeaderPaddings={true}
{...baseProps}
- className={clsx(baseProps.className, styles.root)}
+ className={clsx(baseProps.className, classNames?.root, styles.root)}
__internalRootRef={__internalRootRef}
__contentKey={activeTabId}
disableContentPaddings={true}
@@ -182,7 +183,9 @@ export default function Tabs({
return (
diff --git a/src/tabs/interfaces.ts b/src/tabs/interfaces.ts
index 57090379b8..198fdb41e8 100644
--- a/src/tabs/interfaces.ts
+++ b/src/tabs/interfaces.ts
@@ -109,8 +109,20 @@ export interface TabsProps extends BaseComponentProps {
* @awsuiSystem core
*/
style?: TabsProps.Style;
+
+ /**
+ * An object that maps the tabs' slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The tabs' root element.
+ * @awsuiSystem core
+ */
+ classNames?: TabsProps.ClassNames;
}
export namespace TabsProps {
+ export interface ClassNames {
+ root?: string;
+ }
+
export type Variant = 'default' | 'container' | 'stacked';
export interface Tab {
diff --git a/src/tabs/style-tokens.scss b/src/tabs/style-tokens.scss
new file mode 100644
index 0000000000..c7a5aff3eb
--- /dev/null
+++ b/src/tabs/style-tokens.scss
@@ -0,0 +1,30 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+@use '../internal/styles/style-api' as style-api;
+
+$component: 'tabs';
+$props: style-api.uniform(
+ (
+ active-indicator-color,
+ active-indicator-width,
+ active-indicator-border-radius,
+ background-default,
+ background-hover,
+ background-active,
+ background-disabled,
+ border-color-default,
+ border-color-hover,
+ border-color-active,
+ border-color-disabled,
+ color-default,
+ color-hover,
+ color-active,
+ color-disabled,
+ separator-color,
+ separator-width
+ ),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
diff --git a/src/tabs/styles.scss b/src/tabs/styles.scss
index 7915303b10..11323c0fa9 100644
--- a/src/tabs/styles.scss
+++ b/src/tabs/styles.scss
@@ -5,10 +5,17 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use './style-tokens' as style-tokens;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use './tab-header-bar';
+/* stylelint-disable custom-property-pattern */
+
+// Tabs' public Style API tokens.
+@include style-api.register(style-tokens.$component, style-tokens.$props);
+
.root {
/* used in test-utils or tests */
}
diff --git a/src/tabs/tab-header-bar.scss b/src/tabs/tab-header-bar.scss
index 86f233dee9..d41ae1e77a 100644
--- a/src/tabs/tab-header-bar.scss
+++ b/src/tabs/tab-header-bar.scss
@@ -4,9 +4,12 @@
*/
/* stylelint-disable selector-max-type */
+/* stylelint-disable custom-property-pattern */
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
@use '../internal/styles//foundation' as foundation;
+@use '../internal/styles/style-api' as style-api;
+@use './style-tokens' as style-tokens;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@@ -135,24 +138,30 @@ $label-horizontal-spacing: awsui.$space-xs;
inset-inline-start: 0;
inline-size: calc(100% - 1px);
inset-block-end: calc(-1 * #{awsui.$border-divider-section-width});
- block-size: var(#{custom-props.$styleTabsActiveIndicatorWidth}, awsui.$border-active-width);
+ block-size: var(
+ #{style-api.name(style-tokens.$tokens, active-indicator-width)},
+ var(#{custom-props.$styleTabsActiveIndicatorWidth}, awsui.$border-active-width)
+ );
border-start-start-radius: var(
- #{custom-props.$styleTabsActiveIndicatorBorderRadius},
- awsui.$border-radius-tabs-focus-ring
+ #{style-api.name(style-tokens.$tokens, active-indicator-border-radius)},
+ var(#{custom-props.$styleTabsActiveIndicatorBorderRadius}, awsui.$border-radius-tabs-focus-ring)
);
border-start-end-radius: var(
- #{custom-props.$styleTabsActiveIndicatorBorderRadius},
- awsui.$border-radius-tabs-focus-ring
+ #{style-api.name(style-tokens.$tokens, active-indicator-border-radius)},
+ var(#{custom-props.$styleTabsActiveIndicatorBorderRadius}, awsui.$border-radius-tabs-focus-ring)
);
border-end-start-radius: var(
- #{custom-props.$styleTabsActiveIndicatorBorderRadius},
- awsui.$border-radius-tabs-focus-ring
+ #{style-api.name(style-tokens.$tokens, active-indicator-border-radius)},
+ var(#{custom-props.$styleTabsActiveIndicatorBorderRadius}, awsui.$border-radius-tabs-focus-ring)
);
border-end-end-radius: var(
- #{custom-props.$styleTabsActiveIndicatorBorderRadius},
- awsui.$border-radius-tabs-focus-ring
+ #{style-api.name(style-tokens.$tokens, active-indicator-border-radius)},
+ var(#{custom-props.$styleTabsActiveIndicatorBorderRadius}, awsui.$border-radius-tabs-focus-ring)
+ );
+ background: var(
+ #{style-api.name(style-tokens.$tokens, active-indicator-color)},
+ var(#{custom-props.$styleTabsActiveIndicatorColor}, #{awsui.$color-border-tabs-underline})
);
- background: var(#{custom-props.$styleTabsActiveIndicatorColor}, awsui.$color-border-tabs-underline);
opacity: 0;
}
@@ -172,8 +181,15 @@ $label-horizontal-spacing: awsui.$space-xs;
&:before {
content: '';
position: absolute;
- border-inline-end: var(#{custom-props.$styleTabsSeparatorWidth}, awsui.$border-divider-section-width) solid
- var(#{custom-props.$styleTabsSeparatorColor}, $separator-color);
+ border-inline-end: var(
+ #{style-api.name(style-tokens.$tokens, separator-width)},
+ var(#{custom-props.$styleTabsSeparatorWidth}, awsui.$border-divider-section-width)
+ )
+ solid
+ var(
+ #{style-api.name(style-tokens.$tokens, separator-color)},
+ var(#{custom-props.$styleTabsSeparatorColor}, $separator-color)
+ );
inset: awsui.$space-scaled-s 0;
opacity: 1;
}
@@ -195,14 +211,28 @@ $label-horizontal-spacing: awsui.$space-xs;
padding-inline: 0;
margin-block-start: 1px;
- border-block: awsui.$border-divider-section-width solid var(#{custom-props.$styleBorderColorDefault}, transparent);
- border-inline: awsui.$border-divider-section-width solid var(#{custom-props.$styleBorderColorDefault}, transparent);
+ border-block: awsui.$border-divider-section-width solid
+ var(
+ #{style-api.name(style-tokens.$tokens, border-color-default)},
+ var(#{custom-props.$styleBorderColorDefault}, transparent)
+ );
+ border-inline: awsui.$border-divider-section-width solid
+ var(
+ #{style-api.name(style-tokens.$tokens, border-color-default)},
+ var(#{custom-props.$styleBorderColorDefault}, transparent)
+ );
font-size: awsui.$font-size-tabs;
line-height: awsui.$line-height-tabs;
font-weight: awsui.$font-weight-tabs;
- color: var(#{custom-props.$styleColorDefault}, awsui.$color-text-interactive-default);
- background-color: var(#{custom-props.$styleBackgroundDefault}, transparent);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color-default)},
+ var(#{custom-props.$styleColorDefault}, #{awsui.$color-text-interactive-default})
+ );
+ background-color: var(
+ #{style-api.name(style-tokens.$tokens, background-default)},
+ var(#{custom-props.$styleBackgroundDefault}, transparent)
+ );
padding-inline-start: calc(#{awsui.$space-xxs} - 1px);
padding-inline-end: awsui.$space-xxs;
@@ -216,14 +246,17 @@ $label-horizontal-spacing: awsui.$space-xs;
}
&:hover {
- color: var(#{custom-props.$styleColorHover}, awsui.$color-text-accent);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color-hover)},
+ var(#{custom-props.$styleColorHover}, #{awsui.$color-text-accent})
+ );
border-color: var(
- #{custom-props.$styleBorderColorHover},
- var(#{custom-props.$styleBorderColorDefault}, transparent)
+ #{style-api.name(style-tokens.$tokens, border-color-hover)},
+ var(#{custom-props.$styleBorderColorHover}, var(#{custom-props.$styleBorderColorDefault}, transparent))
);
background-color: var(
- #{custom-props.$styleBackgroundHover},
- var(#{custom-props.$styleBackgroundDefault}, transparent)
+ #{style-api.name(style-tokens.$tokens, background-hover)},
+ var(#{custom-props.$styleBackgroundHover}, var(#{custom-props.$styleBackgroundDefault}, transparent))
);
}
@@ -269,17 +302,35 @@ $label-horizontal-spacing: awsui.$space-xs;
&,
&:hover {
cursor: default;
- color: var(#{custom-props.$styleColorDisabled}, awsui.$color-text-interactive-disabled);
- border-color: var(#{custom-props.$styleBorderColorDisabled}, transparent);
- background-color: var(#{custom-props.$styleBackgroundDisabled}, transparent);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color-disabled)},
+ var(#{custom-props.$styleColorDisabled}, #{awsui.$color-text-interactive-disabled})
+ );
+ border-color: var(
+ #{style-api.name(style-tokens.$tokens, border-color-disabled)},
+ var(#{custom-props.$styleBorderColorDisabled}, transparent)
+ );
+ background-color: var(
+ #{style-api.name(style-tokens.$tokens, background-disabled)},
+ var(#{custom-props.$styleBackgroundDisabled}, transparent)
+ );
font-weight: awsui.$font-weight-tabs-disabled;
}
}
.tabs-tab-active:not(.tabs-tab-disabled) {
- color: var(#{custom-props.$styleColorActive}, awsui.$color-text-accent);
- border-color: var(#{custom-props.$styleBorderColorActive}, transparent);
- background-color: var(#{custom-props.$styleBackgroundActive}, transparent);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color-active)},
+ var(#{custom-props.$styleColorActive}, #{awsui.$color-text-accent})
+ );
+ border-color: var(
+ #{style-api.name(style-tokens.$tokens, border-color-active)},
+ var(#{custom-props.$styleBorderColorActive}, transparent)
+ );
+ background-color: var(
+ #{style-api.name(style-tokens.$tokens, background-active)},
+ var(#{custom-props.$styleBackgroundActive}, transparent)
+ );
&:after {
opacity: 1;
}
@@ -292,3 +343,4 @@ $label-horizontal-spacing: awsui.$space-xs;
.tabs-tab-focusable {
/* used to manage focusable logic */
}
+/* stylelint-enable custom-property-pattern */
diff --git a/src/text-filter/interfaces.ts b/src/text-filter/interfaces.ts
index 3ea4613dd0..efab51e310 100644
--- a/src/text-filter/interfaces.ts
+++ b/src/text-filter/interfaces.ts
@@ -66,6 +66,14 @@ export interface TextFilterProps extends BaseComponentProps, FormFieldControlPro
* @awsuiSystem core
*/
style?: TextFilterProps.Style;
+
+ /**
+ * An object that maps the text filter's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The text filter's root element.
+ * @awsuiSystem core
+ */
+ classNames?: TextFilterProps.ClassNames;
}
export namespace TextFilterProps {
@@ -73,6 +81,10 @@ export namespace TextFilterProps {
filteringText: string;
}
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface Ref {
/**
* Sets focus on the underlying input control.
diff --git a/src/text-filter/internal.tsx b/src/text-filter/internal.tsx
index 90f1abd513..9f5d6ced9b 100644
--- a/src/text-filter/internal.tsx
+++ b/src/text-filter/internal.tsx
@@ -38,6 +38,7 @@ const InternalTextFilter = React.forwardRef(
onDelayedChange,
loading = false,
style,
+ classNames,
__internalRootRef,
...rest
}: InternalTextFilterProps,
@@ -62,7 +63,7 @@ const InternalTextFilter = React.forwardRef(
});
return (
-
+
@@ -107,7 +108,7 @@ const Textarea = React.forwardRef(
return (
diff --git a/src/textarea/interfaces.ts b/src/textarea/interfaces.ts
index 66068c0f98..0f20eb2f01 100644
--- a/src/textarea/interfaces.ts
+++ b/src/textarea/interfaces.ts
@@ -57,11 +57,23 @@ export interface TextareaProps
* @awsuiSystem core
*/
style?: TextareaProps.Style;
+
+ /**
+ * An object that maps the textarea's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The textarea's root element.
+ * @awsuiSystem core
+ */
+ classNames?: TextareaProps.ClassNames;
}
export namespace TextareaProps {
export type KeyDetail = BaseKeyDetail;
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface ChangeDetail {
/**
* The new value of this textarea.
diff --git a/src/textarea/styles.scss b/src/textarea/styles.scss
index 3624bddc81..4d04a741a9 100644
--- a/src/textarea/styles.scss
+++ b/src/textarea/styles.scss
@@ -6,8 +6,12 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
@use '../internal/styles/foundation' as foundation;
+@use '../internal/styles/style-api' as style-api;
+@use '../input/style-tokens' as style-tokens;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
+/* stylelint-disable custom-property-pattern */
+
.root {
/* used for test-utils */
}
@@ -21,80 +25,159 @@
// Allow multi-line placeholders
white-space: pre-wrap;
- padding-block: styles.$control-padding-vertical;
- padding-inline: styles.$control-padding-horizontal;
+ padding-block: var(#{style-api.name(style-tokens.$tokens, padding-block)}, #{styles.$control-padding-vertical});
+ padding-inline: var(#{style-api.name(style-tokens.$tokens, padding-inline)}, #{styles.$control-padding-horizontal});
- color: var(#{custom-props.$styleColorDefault}, awsui.$color-text-body-default);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color)},
+ var(#{custom-props.$styleColorDefault}, awsui.$color-text-body-default)
+ );
max-inline-size: 100%;
inline-size: 100%;
display: block;
box-sizing: border-box;
- background-color: var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default);
- border-start-start-radius: styles.$control-border-radius;
- border-start-end-radius: styles.$control-border-radius;
- border-end-start-radius: styles.$control-border-radius;
- border-end-end-radius: styles.$control-border-radius;
-
- border-block: awsui.$border-width-field solid
- var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default);
- border-inline: awsui.$border-width-field solid
- var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default);
+ background-color: var(
+ #{style-api.name(style-tokens.$tokens, background)},
+ var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default)
+ );
+ border-start-start-radius: var(
+ #{style-api.name(style-tokens.$tokens, border-radius)},
+ #{styles.$control-border-radius}
+ );
+ border-start-end-radius: var(
+ #{style-api.name(style-tokens.$tokens, border-radius)},
+ #{styles.$control-border-radius}
+ );
+ border-end-start-radius: var(
+ #{style-api.name(style-tokens.$tokens, border-radius)},
+ #{styles.$control-border-radius}
+ );
+ border-end-end-radius: var(#{style-api.name(style-tokens.$tokens, border-radius)}, #{styles.$control-border-radius});
+
+ border-block: var(#{style-api.name(style-tokens.$tokens, border-width)}, #{awsui.$border-width-field}) solid
+ var(
+ #{style-api.name(style-tokens.$tokens, border-color)},
+ var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default)
+ );
+ border-inline: var(#{style-api.name(style-tokens.$tokens, border-width)}, #{awsui.$border-width-field}) solid
+ var(
+ #{style-api.name(style-tokens.$tokens, border-color)},
+ var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default)
+ );
- box-shadow: var(#{custom-props.$styleBoxShadowDefault});
+ box-shadow: var(
+ #{style-api.name(style-tokens.$tokens, box-shadow-default)},
+ var(#{custom-props.$styleBoxShadowDefault})
+ );
@include styles.font-body-m;
+ font-size: var(#{style-api.name(style-tokens.$tokens, font-size)}, #{awsui.$font-size-body-m});
+ font-weight: var(#{style-api.name(style-tokens.$tokens, font-weight)}, inherit);
&:hover {
border-color: var(
- #{custom-props.$styleBorderColorHover},
- var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default)
+ #{style-api.name(style-tokens.$tokens, border-color-hover)},
+ var(
+ #{style-api.name(style-tokens.$tokens, border-color)},
+ var(
+ #{custom-props.$styleBorderColorHover},
+ var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-default)
+ )
+ )
);
color: var(
- #{custom-props.$styleColorHover},
- var(#{custom-props.$styleBorderColorDefault}, awsui.$color-text-body-default)
+ #{style-api.name(style-tokens.$tokens, color-hover)},
+ var(
+ #{custom-props.$styleColorHover},
+ var(#{custom-props.$styleBorderColorDefault}, awsui.$color-text-body-default)
+ )
);
background-color: var(
- #{custom-props.$styleBackgroundHover},
- var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default)
+ #{style-api.name(style-tokens.$tokens, background-hover)},
+ var(
+ #{style-api.name(style-tokens.$tokens, background)},
+ var(
+ #{custom-props.$styleBackgroundHover},
+ var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default)
+ )
+ )
+ );
+ box-shadow: var(
+ #{style-api.name(style-tokens.$tokens, box-shadow-hover)},
+ var(#{custom-props.$styleBoxShadowHover}, #{custom-props.$styleBoxShadowDefault})
);
- box-shadow: var(#{custom-props.$styleBoxShadowHover}, #{custom-props.$styleBoxShadowDefault});
}
&.textarea-readonly {
@include styles.form-readonly-element(
$background-color: var(
- #{custom-props.$styleBackgroundReadonly},
- var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default)
+ #{style-api.name(style-tokens.$tokens, background-readonly)},
+ var(
+ #{custom-props.$styleBackgroundReadonly},
+ var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-input-default)
+ )
),
$border-color: var(
- #{custom-props.$styleBorderColorReadonly},
- var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-disabled)
+ #{style-api.name(style-tokens.$tokens, border-color-readonly)},
+ var(
+ #{custom-props.$styleBorderColorReadonly},
+ var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-input-disabled)
+ )
)
);
color: var(
- #{custom-props.$styleColorReadonly},
- var(#{custom-props.$styleColorDefault}, awsui.$color-text-body-default)
+ #{style-api.name(style-tokens.$tokens, color-readonly)},
+ var(#{custom-props.$styleColorReadonly}, var(#{custom-props.$styleColorDefault}, awsui.$color-text-body-default))
+ );
+ box-shadow: var(
+ #{style-api.name(style-tokens.$tokens, box-shadow-readonly)},
+ var(#{custom-props.$styleBoxShadowReadonly})
);
- box-shadow: var(#{custom-props.$styleBoxShadowReadonly});
}
&::placeholder {
@include styles.form-placeholder(
- $color: var(#{custom-props.$stylePlaceholderColor}, awsui.$color-text-input-placeholder),
- $font-size: var(#{custom-props.$stylePlaceholderFontSize}),
- $font-style: var(#{custom-props.$stylePlaceholderFontStyle}, italic),
- $font-weight: var(#{custom-props.$stylePlaceholderFontWeight})
+ $color: var(
+ #{style-api.name(style-tokens.$tokens, placeholder-color)},
+ var(#{custom-props.$stylePlaceholderColor}, awsui.$color-text-input-placeholder)
+ ),
+ $font-size: var(
+ #{style-api.name(style-tokens.$tokens, placeholder-font-size)},
+ var(#{custom-props.$stylePlaceholderFontSize})
+ ),
+ $font-style: var(
+ #{style-api.name(style-tokens.$tokens, placeholder-font-style)},
+ var(#{custom-props.$stylePlaceholderFontStyle}, italic)
+ ),
+ $font-weight: var(
+ #{style-api.name(style-tokens.$tokens, placeholder-font-weight)},
+ var(#{custom-props.$stylePlaceholderFontWeight})
+ )
);
opacity: 1;
}
&:focus {
@include styles.form-focus-element(
- $border-color: var(#{custom-props.$styleBorderColorFocus}, awsui.$color-border-input-focused),
- $box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light)
+ $border-radius: var(#{style-api.name(style-tokens.$tokens, border-radius)}, #{styles.$control-border-radius}),
+ $border-width: var(#{style-api.name(style-tokens.$tokens, border-width)}, #{awsui.$border-width-field}),
+ $border-color: var(
+ #{style-api.name(style-tokens.$tokens, border-color-focus)},
+ var(#{custom-props.$styleBorderColorFocus}, awsui.$color-border-input-focused)
+ ),
+ $box-shadow: var(
+ #{style-api.name(style-tokens.$tokens, focus-ring-shadow)},
+ var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light)
+ )
+ );
+ color: var(
+ #{style-api.name(style-tokens.$tokens, color-focus)},
+ var(#{custom-props.$styleColorFocus}, awsui.$color-text-body-default)
+ );
+ background-color: var(
+ #{style-api.name(style-tokens.$tokens, background-focus)},
+ var(#{custom-props.$styleBackgroundFocus}, awsui.$color-background-input-default)
);
- color: var(#{custom-props.$styleColorFocus}, awsui.$color-text-body-default);
- background-color: var(#{custom-props.$styleBackgroundFocus}, awsui.$color-background-input-default);
}
&:invalid {
@@ -104,12 +187,24 @@
&:disabled {
@include styles.form-disabled-element(
- $background-color: var(#{custom-props.$styleBackgroundDisabled}, awsui.$color-background-input-disabled),
- $border-color: var(#{custom-props.$styleBorderColorDisabled}, awsui.$color-border-input-disabled),
- $color: var(#{custom-props.$styleColorDisabled}, awsui.$color-text-input-disabled),
+ $background-color: var(
+ #{style-api.name(style-tokens.$tokens, background-disabled)},
+ var(#{custom-props.$styleBackgroundDisabled}, awsui.$color-background-input-disabled)
+ ),
+ $border-color: var(
+ #{style-api.name(style-tokens.$tokens, border-color-disabled)},
+ var(#{custom-props.$styleBorderColorDisabled}, awsui.$color-border-input-disabled)
+ ),
+ $color: var(
+ #{style-api.name(style-tokens.$tokens, color-disabled)},
+ var(#{custom-props.$styleColorDisabled}, awsui.$color-text-input-disabled)
+ ),
$cursor: default
);
- box-shadow: var(#{custom-props.$styleBoxShadowDisabled});
+ box-shadow: var(
+ #{style-api.name(style-tokens.$tokens, box-shadow-disabled)},
+ var(#{custom-props.$styleBoxShadowDisabled})
+ );
&::placeholder {
@include styles.form-placeholder-disabled;
opacity: 1;
diff --git a/src/toggle/interfaces.ts b/src/toggle/interfaces.ts
index c5e4848eab..0681af1068 100644
--- a/src/toggle/interfaces.ts
+++ b/src/toggle/interfaces.ts
@@ -29,6 +29,14 @@ export interface ToggleProps extends BaseCheckboxProps {
*/
style?: ToggleProps.Style;
+ /**
+ * An object that maps the toggle's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The toggle's root element.
+ * @awsuiSystem core
+ */
+ classNames?: ToggleProps.ClassNames;
+
/**
* Attributes to add to the native `input` element.
* Some attributes will be automatically combined with internal attribute values:
@@ -50,6 +58,10 @@ export namespace ToggleProps {
focus(): void;
}
+ export interface ClassNames {
+ root?: string;
+ }
+
export interface ChangeDetail {
checked: boolean;
}
diff --git a/src/toggle/internal.tsx b/src/toggle/internal.tsx
index 480be02bdf..171aef7ada 100644
--- a/src/toggle/internal.tsx
+++ b/src/toggle/internal.tsx
@@ -43,6 +43,7 @@ const InternalToggle = React.forwardRef(
nativeInputAttributes,
__internalRootRef,
style,
+ classNames,
__injectAnalyticsComponentMetadata,
...rest
},
@@ -73,7 +74,7 @@ const InternalToggle = React.forwardRef(
return (
.dismiss-button {
- color: var(#{custom-props.$tokenStyleDismissColorReadOnly}, awsui.$color-text-button-inline-icon-disabled);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, dismiss-color-readonly)},
+ var(#{custom-props.$tokenStyleDismissColorReadOnly}, awsui.$color-text-button-inline-icon-disabled)
+ );
}
}
.token-box-disabled {
pointer-events: none;
- border-color: var(#{custom-props.$tokenStyleBorderColorDisabled}, awsui.$color-border-control-disabled);
- background: var(#{custom-props.$tokenStyleBackgroundDisabled}, awsui.$color-background-container-content);
+ border-color: var(
+ #{style-api.name(style-tokens.$tokens, border-color-disabled)},
+ var(#{custom-props.$tokenStyleBorderColorDisabled}, awsui.$color-border-control-disabled)
+ );
+ background: var(
+ #{style-api.name(style-tokens.$tokens, background-disabled)},
+ var(#{custom-props.$tokenStyleBackgroundDisabled}, awsui.$color-background-container-content)
+ );
color: awsui.$color-text-disabled;
// stylelint-disable-next-line no-descending-specificity
> .dismiss-button {
- color: var(#{custom-props.$tokenStyleDismissColorDisabled}, awsui.$color-text-button-inline-icon-disabled);
+ color: var(
+ #{style-api.name(style-tokens.$tokens, dismiss-color-disabled)},
+ var(#{custom-props.$tokenStyleDismissColorDisabled}, awsui.$color-text-button-inline-icon-disabled)
+ );
}
}
diff --git a/src/top-navigation/interfaces.ts b/src/top-navigation/interfaces.ts
index 3bad629d32..9aaa6b2bf9 100644
--- a/src/top-navigation/interfaces.ts
+++ b/src/top-navigation/interfaces.ts
@@ -66,9 +66,23 @@ export interface TopNavigationProps extends BaseComponentProps {
* @i18n
*/
i18nStrings?: TopNavigationProps.I18nStrings;
+
+ /**
+ * An object that maps the top navigation's slots to CSS class names for custom styling.
+ * Use these classes to scope `--awsui-style-*` custom properties.
+ * * `root` - The top navigation's root element.
+ * * `utility` - Applied to each utility wrapper. Accepts a string or a function receiving `{ utility }` and returning a string.
+ * @awsuiSystem core
+ */
+ classNames?: TopNavigationProps.ClassNames;
}
export namespace TopNavigationProps {
+ export interface ClassNames {
+ root?: string;
+ utility?: string | ((args: { utility: TopNavigationProps.Utility }) => string);
+ }
+
export interface Identity {
title?: string;
logo?: Logo;
@@ -93,6 +107,10 @@ export namespace TopNavigationProps {
badge?: boolean;
disableUtilityCollapse?: boolean;
disableTextCollapse?: boolean;
+ /**
+ * @deprecated Use `classNames.utility` instead.
+ */
+ className?: string;
}
export interface MenuDropdownUtility extends BaseUtility {
diff --git a/src/top-navigation/internal.tsx b/src/top-navigation/internal.tsx
index eea5af6761..774839a548 100644
--- a/src/top-navigation/internal.tsx
+++ b/src/top-navigation/internal.tsx
@@ -27,6 +27,7 @@ export default function InternalTopNavigation({
i18nStrings,
utilities,
search,
+ classNames,
...restProps
}: InternalTopNavigationProps) {
checkSafeUrl('TopNavigation', identity.href);
@@ -47,6 +48,18 @@ export default function InternalTopNavigation({
}
};
+ const resolvedUtilities = utilities.map(utility => ({
+ ...utility,
+ // Attach the menu bridge class (styles['utility-style']) so the utility's menu trigger/items are
+ // driven by top-navigation's tokens; compose any consumer classNames.utility on top.
+ className:
+ clsx(
+ styles['utility-style'],
+ utility.className,
+ typeof classNames?.utility === 'function' ? classNames.utility({ utility }) : classNames?.utility
+ ) || undefined,
+ }));
+
const toggleOverflowMenu = () => {
setOverflowMenuOpen(overflowMenuOpen => !overflowMenuOpen);
};
@@ -142,14 +155,14 @@ export default function InternalTopNavigation({
)}
{showUtilities &&
- utilities
+ resolvedUtilities
.filter(
(_utility, i) =>
isVirtual || !responsiveState.hideUtilities || responsiveState.hideUtilities.indexOf(i) === -1
)
.map((utility, i) => {
const hideText = !!responsiveState.hideUtilityText;
- const isLast = (isVirtual || !showMenuTrigger) && i === utilities.length - 1;
+ const isLast = (isVirtual || !showMenuTrigger) && i === resolvedUtilities.length - 1;
const offsetRight = isLast && isLargeViewport ? 'xxl' : isLast ? 'l' : undefined;
return (
@@ -173,9 +186,9 @@ export default function InternalTopNavigation({
})}
{isVirtual &&
- utilities.map((utility, i) => {
+ resolvedUtilities.map((utility, i) => {
const hideText = !responsiveState.hideUtilityText;
- const isLast = !showMenuTrigger && i === utilities.length - 1;
+ const isLast = !showMenuTrigger && i === resolvedUtilities.length - 1;
const offsetRight = isLast && isLargeViewport ? 'xxl' : isLast ? 'l' : undefined;
return (
@@ -223,7 +236,7 @@ export default function InternalTopNavigation({
};
return (
-
+
{/* Render virtual content first to ensure React refs for content will be assigned on the actual nodes. */}
{content(true)}
@@ -236,7 +249,7 @@ export default function InternalTopNavigation({
headerText={i18nStrings?.overflowMenuTitleText}
dismissIconAriaLabel={i18nStrings?.overflowMenuDismissIconAriaLabel}
backIconAriaLabel={i18nStrings?.overflowMenuBackIconAriaLabel}
- items={utilities.filter(
+ items={resolvedUtilities.filter(
(utility, i) =>
(!responsiveState.hideUtilities || responsiveState.hideUtilities.indexOf(i) !== -1) &&
!utility.disableUtilityCollapse
diff --git a/src/top-navigation/parts/utility.tsx b/src/top-navigation/parts/utility.tsx
index 6fc0df1003..d63420e40d 100644
--- a/src/top-navigation/parts/utility.tsx
+++ b/src/top-navigation/parts/utility.tsx
@@ -34,7 +34,7 @@ export default function Utility({ hideText, definition, offsetRight }: UtilityPr
checkSafeUrl('TopNavigation', definition.href);
if (definition.variant === 'primary-button') {
return (
-
+
+
- {!shouldHideText && definition.text}
-
+
+
+ {!shouldHideText && definition.text}
+
+
);
}
diff --git a/src/top-navigation/styles.scss b/src/top-navigation/styles.scss
index 6167698a8a..8801445270 100644
--- a/src/top-navigation/styles.scss
+++ b/src/top-navigation/styles.scss
@@ -5,11 +5,45 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use '../internal/components/menu-dropdown/mixins' as menu-dropdown;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+/* stylelint-disable custom-property-pattern */
+
+// Top-navigation owns its tokens, including the menu trigger and menu items rendered by the shared
+// menu-dropdown (impl detail). The embedded Input/Button/Link utilities are themed via their own
+// `--awsui-style-{component}-*` tokens through classNames.
+$component: 'top-navigation';
+$props: style-api.uniform(
+ (
+ background,
+ title-color,
+ title-color-hover,
+ menu-trigger-background-active,
+ menu-trigger-background-hover,
+ menu-trigger-color,
+ menu-trigger-color-active,
+ menu-trigger-color-hover,
+ menu-item-color,
+ menu-item-color-highlighted,
+ menu-item-background-hover
+ ),
+ $inherits: true
+);
+$tokens: style-api.resolve($component, $props);
+
+@include style-api.register($component, $props);
+
+// Bridge class applied by top-navigation to each utility, mapping its menu tokens to the
+// menu-dropdown internal vars (per-utility so per-utility overrides resolve on the same element).
+.utility-style {
+ @include menu-dropdown.host($tokens);
+}
+
.top-navigation {
@include styles.styles-reset;
- background: awsui.$color-background-container-content;
+ background: var(#{style-api.name($tokens, background)}, #{awsui.$color-background-container-content});
border-block-end: awsui.$border-divider-section-width solid awsui.$color-border-divider-default;
> .padding-box {
@@ -62,10 +96,10 @@
display: flex;
align-items: center;
text-decoration: none;
- color: awsui.$color-text-top-navigation-title;
+ color: var(#{style-api.name($tokens, title-color)}, #{awsui.$color-text-top-navigation-title});
&:hover {
- color: awsui.$color-text-accent;
+ color: var(#{style-api.name($tokens, title-color-hover)}, #{awsui.$color-text-accent});
}
@include focus-visible.when-visible {
@@ -217,7 +251,7 @@
.overflow-menu {
@include styles.styles-reset;
- background: awsui.$color-background-container-content;
+ background: var(#{style-api.name($tokens, background)}, #{awsui.$color-background-container-content});
block-size: 100%;
}