diff --git a/src/panel-layout/styles.scss b/src/panel-layout/styles.scss
index 62e17be8c4..fcd953c53a 100644
--- a/src/panel-layout/styles.scss
+++ b/src/panel-layout/styles.scss
@@ -30,6 +30,17 @@
.panel {
display: flex;
flex-shrink: 0;
+ @include styles.with-motion {
+ transition: inline-size awsui.$motion-duration-complex awsui.$motion-easing-responsive;
+ }
+ &-resizing {
+ @include styles.with-motion {
+ transition: none;
+ }
+ > .panel-content {
+ pointer-events: none;
+ }
+ }
> .handle {
display: flex;
align-items: center;
@@ -59,6 +70,9 @@
.display-panel-only > & {
display: none;
}
+ .panel-resizing ~ & {
+ pointer-events: none;
+ }
@include focus-visible.when-visible {
@include container-inner-focus-ring;
}
diff --git a/src/side-navigation/implementation.tsx b/src/side-navigation/implementation.tsx
index 4b2d11a066..80c73065ce 100644
--- a/src/side-navigation/implementation.tsx
+++ b/src/side-navigation/implementation.tsx
@@ -24,6 +24,9 @@ export function SideNavigationImplementation({
items = [],
onFollow,
onChange,
+ expandIconPosition = 'start',
+ collapsed = false,
+ variant = 'default',
__internalRootRef,
...props
}: SideNavigationInternalProps) {
@@ -66,9 +69,15 @@ export function SideNavigationImplementation({
ref={__internalRootRef}
>
{header && (
-
+
)}
- {itemsControl &&
{itemsControl}
}
+ {!collapsed && itemsControl &&
{itemsControl}
}
{items && (
)}
diff --git a/src/side-navigation/index.tsx b/src/side-navigation/index.tsx
index ea6ff5c36c..f0a7ccfd1d 100644
--- a/src/side-navigation/index.tsx
+++ b/src/side-navigation/index.tsx
@@ -15,8 +15,16 @@ import analyticsSelectors from './analytics-metadata/styles.css.js';
export { SideNavigationProps };
-export default function SideNavigation({ items = [], ...props }: SideNavigationProps) {
- const internalProps = useBaseComponent('SideNavigation');
+export default function SideNavigation({
+ items = [],
+ expandIconPosition = 'start',
+ collapsed = false,
+ variant = 'default',
+ ...props
+}: SideNavigationProps) {
+ const internalProps = useBaseComponent('SideNavigation', {
+ props: { expandIconPosition, collapsed, variant },
+ });
const componentAnalyticMetadata: GeneratedAnalyticsMetadataSideNavigationComponent = {
name: 'awsui.SideNavigation',
@@ -30,6 +38,9 @@ export default function SideNavigation({ items = [], ...props }: SideNavigationP
{...props}
{...internalProps}
items={items}
+ expandIconPosition={expandIconPosition}
+ collapsed={collapsed}
+ variant={variant}
{...getAnalyticsMetadataAttribute({ component: componentAnalyticMetadata })}
/>
);
diff --git a/src/side-navigation/interfaces.tsx b/src/side-navigation/interfaces.tsx
index c3241d9bc5..e6f378b026 100644
--- a/src/side-navigation/interfaces.tsx
+++ b/src/side-navigation/interfaces.tsx
@@ -52,6 +52,7 @@ export interface SideNavigationProps extends BaseComponentProps {
* - `externalIconAriaLabel` (string) - Adds an aria-label to the external icon.
* - `info` (ReactNode) - Enables you to display content next to the link. Although it is technically possible to insert any content,
* our UX guidelines allow only to add a Badge and/or a "New" label.
+ * - `icon` (ReactNode) - Optional content rendered before the link text. Accepts any React node (for example, an `
` component or a custom `
`).
*
* #### Divider
* Object that represents a horizontal divider between navigation content.
@@ -65,6 +66,7 @@ export interface SideNavigationProps extends BaseComponentProps {
* - `items` (array) - Specifies the content of the section. You can use any valid item from this list.
* Although there is no technical limitation to the nesting level,
* our UX recommendation is to use only one level.
+ * - `icon` (ReactNode) - Optional content rendered before the section title. Accepts any React node.
*
* #### Section Group
* Aggregates a set of items that are conceptually related to each other, and can be displayed under a single heading to provide further organization.
@@ -72,6 +74,7 @@ export interface SideNavigationProps extends BaseComponentProps {
* - `type`: `'section-group'`.
* - `title` (string) - Specifies the text to display as a title of the section group.
* - `items` (array) - Specifies the content of the section header group. You can use `Section`, `Link`, `LinkGroup`, `ExpandableLinkGroup`.
+ * - `icon` (ReactNode) - Optional content rendered before the section group title. Accepts any React node.
*
* #### LinkGroup
* Object that represents a group of links.
@@ -83,6 +86,7 @@ export interface SideNavigationProps extends BaseComponentProps {
* - `items` (array) - Specifies the content of the section. You can use any valid item from this list.
* Although there is no technical limitation to the nesting level,
* our UX recommendation is to use only one level.
+ * - `icon` (ReactNode) - Optional content rendered before the link group text. Accepts any React node.
*
* #### ExpandableLinkGroup
*
@@ -95,6 +99,7 @@ export interface SideNavigationProps extends BaseComponentProps {
* - `items` (array) - Specifies the content of the section. You can use any valid item from this list.
* Although there is no technical limitation to the nesting level,
* our UX recommendation is to use only one level.
+ * - `icon` (ReactNode) - Optional content rendered before the expandable link group text. Accepts any React node.
*/
items?: ReadonlyArray;
@@ -124,12 +129,47 @@ export interface SideNavigationProps extends BaseComponentProps {
* upon changing the `activeHref` property, this event isn't raised.
*/
onChange?: NonCancelableEventHandler;
+
+ /**
+ * Controls the placement of the expand/collapse icon for `Section` and
+ * `ExpandableLinkGroup` items.
+ * - `start` (default) - The icon is rendered before the section title.
+ * - `end` - The icon is rendered after the section title and pulled to the
+ * inline-end of a full-width header.
+ */
+ expandIconPosition?: SideNavigationProps.ExpandIconPosition;
+
+ /**
+ * If true, the navigation is rendered in a compact, icon-only state:
+ * - Item text labels and the header title are hidden.
+ * - Section, `Section group`, `Link group`, and `Expandable link group`
+ * children are not rendered.
+ * - `Items control` is not rendered.
+ * - Items without an `icon` are not rendered. Provide an icon for any
+ * item that should remain visible while collapsed.
+ *
+ * @defaultValue false
+ */
+ collapsed?: boolean;
+
+ /**
+ * Visual variant of the navigation:
+ * - `default` (default) - The active item uses a text-highlight style.
+ * - `highlighted` - The active item uses a background fill with rounded
+ * corners. Each item is padded so the background fills a comfortable area
+ * around the label and icon.
+ */
+ variant?: SideNavigationProps.Variant;
}
export namespace SideNavigationProps {
+ export type ExpandIconPosition = 'start' | 'end';
+ export type Variant = 'default' | 'highlighted';
+
export interface Logo {
- src: string;
+ src?: string;
alt?: string;
+ svg?: React.ReactNode;
}
export interface Header {
text?: string;
@@ -148,6 +188,7 @@ export namespace SideNavigationProps {
external?: boolean;
externalIconAriaLabel?: string;
info?: React.ReactNode;
+ icon?: React.ReactNode;
}
export interface Section {
@@ -155,12 +196,14 @@ export namespace SideNavigationProps {
text: string;
items: ReadonlyArray- ;
defaultExpanded?: boolean;
+ icon?: React.ReactNode;
}
export interface SectionGroup {
type: 'section-group';
title: string;
items: ReadonlyArray
;
+ icon?: React.ReactNode;
}
export interface LinkGroup {
type: 'link-group';
@@ -168,6 +211,7 @@ export namespace SideNavigationProps {
href: string;
info?: React.ReactNode;
items: ReadonlyArray- ;
+ icon?: React.ReactNode;
}
export interface ExpandableLinkGroup {
@@ -176,6 +220,7 @@ export namespace SideNavigationProps {
href: string;
items: ReadonlyArray
- ;
defaultExpanded?: boolean;
+ icon?: React.ReactNode;
}
export type Item = Divider | Link | Section | LinkGroup | ExpandableLinkGroup | SectionGroup;
diff --git a/src/side-navigation/parts.tsx b/src/side-navigation/parts.tsx
index 484d42494a..0d6f14f89c 100644
--- a/src/side-navigation/parts.tsx
+++ b/src/side-navigation/parts.tsx
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React, { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
+import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import clsx from 'clsx';
import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
@@ -13,6 +13,7 @@ import InternalIcon from '../icon/internal';
import { isPlainLeftClick, NonCancelableCustomEvent } from '../internal/events';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { checkSafeUrl } from '../internal/utils/check-safe-url';
+import Tooltip from '../tooltip/internal';
import { GeneratedAnalyticsMetadataSideNavigationClick } from './analytics-metadata/interfaces';
import { SideNavigationProps } from './interfaces';
import { hasActiveLink } from './util';
@@ -33,13 +34,18 @@ interface BaseItemComponentProps {
event: React.SyntheticEvent | Event
) => void;
position?: string;
+ expandIconPosition?: SideNavigationProps.ExpandIconPosition;
+ collapsed?: boolean;
+ // Renamed from `variant` (which is already used for the list variant) so we
+ // can plumb the SideNavigation public `variant` prop down to each link.
+ highlightVariant?: SideNavigationProps.Variant;
}
interface HeaderProps extends BaseItemComponentProps {
definition: SideNavigationProps.Header;
}
-export function Header({ definition, activeHref, fireFollow }: HeaderProps) {
+export function Header({ definition, activeHref, fireFollow, collapsed }: HeaderProps) {
checkSafeUrl('SideNavigation', definition.href);
const onClick = useCallback(
(event: React.MouseEvent) => {
@@ -62,25 +68,38 @@ export function Header({ definition, activeHref, fireFollow }: HeaderProps) {
return (
<>
-
+
@@ -106,6 +125,9 @@ export function NavigationItemsList({
fireChange,
fireFollow,
position = '',
+ expandIconPosition,
+ collapsed,
+ highlightVariant,
}: NavigationItemsListProps) {
const lists: Array- = [];
let currentListIndex = 0;
@@ -117,13 +139,53 @@ export function NavigationItemsList({
items.forEach((item, index) => {
const itemid = index + 1;
const itemPosition = `${position ? `${position},` : ''}${itemid}`;
+ // In collapsed mode, only selectable items are visible (links, link-groups, ELGs).
+ // Sections and section-groups are non-selectable containers — skip them.
+ if (collapsed && (item.type === 'section' || item.type === 'section-group')) {
+ // Flatten icon-bearing children into the current list
+ const childItems: ReadonlyArray
=
+ item.type === 'section'
+ ? (item as SideNavigationProps.Section).items
+ : (item as SideNavigationProps.SectionGroup).items.flatMap(child =>
+ child.type === 'section' ? (child as SideNavigationProps.Section).items : [child]
+ );
+ childItems.forEach((child, childIndex) => {
+ const childLink = child as
+ | SideNavigationProps.Link
+ | SideNavigationProps.LinkGroup
+ | SideNavigationProps.ExpandableLinkGroup;
+ if (!childLink.icon) {
+ return;
+ }
+ const childPosition = `${position ? `${position},` : ''}${itemid},${childIndex + 1}`;
+ lists[currentListIndex].items?.push({
+ element: (
+
+
+
+ ),
+ });
+ });
+ return;
+ }
+ if (collapsed && item.type !== 'divider' && !item.icon) {
+ return;
+ }
switch (item.type) {
case 'divider': {
const dividerIndex = lists.length;
lists[dividerIndex] = {
element: (
),
};
@@ -137,13 +199,19 @@ export function NavigationItemsList({
case 'link': {
lists[currentListIndex].items?.push({
element: (
-
+
),
@@ -153,7 +221,11 @@ export function NavigationItemsList({
case 'section': {
lists[currentListIndex].items?.push({
element: (
-
+
),
@@ -170,13 +245,20 @@ export function NavigationItemsList({
case 'section-group': {
lists[currentListIndex].items?.push({
element: (
-
+
),
@@ -186,13 +268,20 @@ export function NavigationItemsList({
case 'link-group': {
lists[currentListIndex].items?.push({
element: (
-
+
),
@@ -202,7 +291,11 @@ export function NavigationItemsList({
case 'expandable-link-group': {
lists[currentListIndex].items?.push({
element: (
-
+
),
@@ -219,15 +315,34 @@ export function NavigationItemsList({
}
});
+ // In collapsed mode, skip empty item segments and deduplicate consecutive dividers.
+ const filteredLists = collapsed
+ ? lists.filter((list, index) => {
+ if (list.items) {
+ return list.items.length > 0;
+ }
+ // Divider — skip if preceded by another divider or empty segment.
+ const prevVisible = lists
+ .slice(0, index)
+ .reverse()
+ .find(l => !l.items || l.items.length > 0);
+ return !prevVisible || (prevVisible.items !== undefined && prevVisible.items.length > 0);
+ })
+ : lists;
+
return (
<>
- {lists.map((list, index) => {
+ {filteredLists.map((list, index) => {
if (!list.items || list.items.length === 0) {
return (
{list.element}
@@ -239,6 +354,11 @@ export function NavigationItemsList({
key={`list-${index}`}
className={clsx(styles.list, styles[`list-variant-${list.listVariant}`], {
[styles['list-variant-root--first']]: list.listVariant === 'root' && index === 0,
+ [styles['list-variant-root--collapsed']]: list.listVariant === 'root' && collapsed,
+ [styles['list-variant-root--symmetric']]:
+ list.listVariant === 'root' &&
+ (collapsed || expandIconPosition === 'end' || highlightVariant === 'highlighted'),
+ [styles[`expand-icon-end`]]: expandIconPosition === 'end',
})}
>
{list.items.map(item => item.element)}
@@ -253,12 +373,13 @@ export function NavigationItemsList({
interface DividerProps {
variant: 'default' | 'header';
isPresentational?: boolean;
+ collapsed?: boolean;
}
-function Divider({ variant = 'default', isPresentational = false }: DividerProps) {
+function Divider({ variant = 'default', isPresentational = false, collapsed }: DividerProps) {
return (
);
@@ -268,10 +389,61 @@ interface LinkProps extends BaseItemComponentProps {
definition: SideNavigationProps.Link;
}
-function Link({ definition, activeHref, fireFollow, position }: LinkProps) {
+interface ItemIconProps extends React.HTMLAttributes
{
+ icon: React.ReactNode;
+ collapsed?: boolean;
+}
+
+const ItemIcon = React.forwardRef(function ItemIcon(
+ { icon, collapsed, className, ...rest },
+ ref
+) {
+ if (!icon) {
+ return null;
+ }
+ return (
+
+ {icon}
+
+ );
+});
+
+// Manages a tooltip that shows the item's text label on focus or hover.
+// Used in the collapsed state, where the visible labels are hidden, to give
+// pointer and keyboard users a way to identify each item without relying on
+// their browser's native title popup.
+function useCollapsedTooltip(label: React.ReactNode) {
+ const [show, setShow] = useState(false);
+ const triggerRef = useRef(null);
+
+ const triggerProps = {
+ onFocus: () => setShow(true),
+ onBlur: () => setShow(false),
+ onMouseEnter: () => setShow(true),
+ onMouseLeave: () => setShow(false),
+ };
+
+ const tooltip = show ? (
+ triggerRef.current} content={label} position="right" onEscape={() => setShow(false)} />
+ ) : null;
+
+ return { triggerRef, triggerProps, tooltip };
+}
+
+function Link({ definition, activeHref, fireFollow, position, collapsed, highlightVariant }: LinkProps) {
checkSafeUrl('SideNavigation', definition.href);
const isActive = definition.href === activeHref;
const i18n = useInternalI18n('link');
+ const collapsedTooltip = useCollapsedTooltip(definition.text);
const onClick = useCallback(
(event: React.MouseEvent) => {
@@ -297,22 +469,33 @@ function Link({ definition, activeHref, fireFollow, position }: LinkProps) {
return (
<>
- {definition.text}
- {definition.external && (
+
+ {!collapsed && {definition.text} }
+ {!collapsed && definition.external && (
)}
- {definition.info && {definition.info} }
+ {!collapsed && definition.info && (
+ {definition.info}
+ )}
+ {collapsed && collapsedTooltip.tooltip}
>
);
}
@@ -322,9 +505,20 @@ interface SectionProps extends BaseItemComponentProps {
variant: 'section' | 'section-group' | 'link-group' | 'expandable-link-group' | 'root';
}
-function Section({ definition, activeHref, fireFollow, fireChange, variant, position }: SectionProps) {
+function Section({
+ definition,
+ activeHref,
+ fireFollow,
+ fireChange,
+ variant,
+ position,
+ expandIconPosition,
+ collapsed,
+ highlightVariant,
+}: SectionProps) {
const [expanded, setExpanded] = useState(definition.defaultExpanded ?? true);
const isVisualRefresh = useVisualRefresh();
+ const collapsedTooltip = useCollapsedTooltip(definition.text);
const onExpandedChange = useCallback(
(e: NonCancelableCustomEvent) => {
@@ -338,6 +532,23 @@ function Section({ definition, activeHref, fireFollow, fireChange, variant, posi
setExpanded(definition.defaultExpanded ?? true);
}, [definition]);
+ if (collapsed) {
+ return (
+ <>
+
+ {collapsedTooltip.tooltip}
+ >
+ );
+ }
+
+ const isNestedInSectionGroup = variant === 'section-group';
+
return (
+
+ {definition.text}
+
+ ) : (
+ definition.text
+ )
+ }
+ __expandIconPosition={expandIconPosition}
>
);
@@ -366,10 +590,36 @@ interface SectionGroupProps extends BaseItemComponentProps {
definition: SideNavigationProps.SectionGroup;
}
-function SectionGroup({ definition, activeHref, fireFollow, fireChange, position }: SectionGroupProps) {
+function SectionGroup({
+ definition,
+ activeHref,
+ fireFollow,
+ fireChange,
+ position,
+ expandIconPosition,
+ collapsed,
+ highlightVariant,
+}: SectionGroupProps) {
+ const collapsedTooltip = useCollapsedTooltip(definition.title);
+
+ if (collapsed) {
+ return (
+ <>
+
+ {collapsedTooltip.tooltip}
+ >
+ );
+ }
return (
+
{definition.title}
);
@@ -388,26 +640,47 @@ interface LinkGroupProps extends BaseItemComponentProps {
definition: SideNavigationProps.LinkGroup;
}
-function LinkGroup({ definition, activeHref, fireFollow, fireChange, position }: LinkGroupProps) {
+function LinkGroup({
+ definition,
+ activeHref,
+ fireFollow,
+ fireChange,
+ position,
+ expandIconPosition,
+ collapsed,
+ highlightVariant,
+}: LinkGroupProps) {
checkSafeUrl('SideNavigation', definition.href);
return (
<>
fireFollow(definition, event)}
fireChange={fireChange}
activeHref={activeHref}
position={position}
+ collapsed={collapsed}
+ highlightVariant={highlightVariant}
/>
-
+ {!collapsed && (
+
+ )}
>
);
}
@@ -424,6 +697,9 @@ function ExpandableLinkGroup({
activeHref,
variant,
position,
+ expandIconPosition,
+ collapsed,
+ highlightVariant,
}: ExpandableLinkGroupProps) {
// Check whether the definition contains an active link and memoize it to avoid
// rechecking every time.
@@ -468,24 +744,43 @@ function ExpandableLinkGroup({
}
};
+ if (collapsed) {
+ return (
+
+ );
+ }
+
return (
}
+ __expandIconPosition={expandIconPosition}
>
);
diff --git a/src/side-navigation/styles.scss b/src/side-navigation/styles.scss
index 1595911163..730b239f41 100644
--- a/src/side-navigation/styles.scss
+++ b/src/side-navigation/styles.scss
@@ -7,11 +7,37 @@
@use '../internal/styles/tokens' as awsui;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+// ==========================================================================
+// Item sizing — single source of truth for navigation density.
+// Adjust $item-height to scale all items uniformly.
+// ==========================================================================
+$item-icon-text-gap: awsui.$space-xs;
+$item-height: awsui.$size-side-navigation-item-height;
+$item-margin-block: awsui.$space-side-navigation-item-gap;
+$item-collapsed-gap: awsui.$space-side-navigation-item-collapsed-gap;
+$item-padding-inline: awsui.$space-xs;
+$item-indent: calc(16px + #{$item-icon-text-gap} - #{$item-padding-inline});
+$expandable-icon-negative-margin: 19px;
+
+@mixin item-radius {
+ border-start-start-radius: awsui.$border-radius-item;
+ border-start-end-radius: awsui.$border-radius-item;
+ border-end-start-radius: awsui.$border-radius-item;
+ border-end-end-radius: awsui.$border-radius-item;
+}
+
.root {
@include styles.styles-reset;
@include styles.text-wrapping;
}
+.with-toolbar {
+ /* Structural class — parent context for toolbar margin resets. */
+}
+
+// ==========================================================================
+// Header
+// ==========================================================================
.header {
@include styles.font-panel-header;
margin-block: 0;
@@ -20,13 +46,19 @@
padding-inline-start: awsui.$space-panel-nav-left;
// Additional xl space to prevent text from overlapping the close button.
padding-inline-end: calc(#{awsui.$space-scaled-xxl} + #{awsui.$space-xl});
+
+ &--collapsed {
+ padding-inline: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
}
.header-link {
@include styles.font-panel-header;
color: awsui.$color-text-heading-default;
min-block-size: awsui.$font-panel-header-line-height;
-
display: flex;
&--has-logo {
@@ -49,6 +81,9 @@
}
}
+// ==========================================================================
+// List structure
+// ==========================================================================
.items-control {
padding-inline: awsui.$space-l;
}
@@ -71,93 +106,215 @@
margin-inline: 0;
padding-block: 0;
padding-inline-end: 0;
- padding-inline-start: awsui.$space-l;
+ padding-inline-start: $item-indent;
}
.list-variant-root {
margin-block: 0;
margin-inline: 0;
padding-block: 0;
- padding-inline-start: awsui.$space-panel-nav-left;
- padding-inline-end: awsui.$space-panel-side-right;
+ padding-inline-start: calc(#{awsui.$space-panel-nav-left} - #{$item-padding-inline});
+ padding-inline-end: calc(#{awsui.$space-panel-side-right} - #{$item-padding-inline});
&--first {
margin-block-start: 0;
}
+
+ &--symmetric {
+ padding-inline-start: calc(#{awsui.$space-panel-side-right} - #{$item-padding-inline});
+ }
+
+ &--collapsed {
+ padding-inline-start: 0;
+ padding-inline-end: 0;
+ }
}
.list-variant-expandable-link-group {
- padding-inline-start: awsui.$space-xxxl;
+ // 2 levels of nested indent (header + child) + icon/text offset.
+ padding-inline-start: calc(#{$item-indent} + #{$expandable-icon-negative-margin});
+ &.expand-icon-end {
+ padding-inline-start: $item-indent;
+ }
+}
+
+.list-variant-section {
+ padding-inline-start: calc(#{$expandable-icon-negative-margin} - #{$item-padding-inline});
+ &.expand-icon-end {
+ padding-inline-start: 0;
+ margin-inline-start: calc(-1 * #{$item-padding-inline});
+ }
+}
+
+.list-variant-link-group {
+ padding-inline-start: $item-indent;
+}
+
+.list-variant-section-group {
+ margin-block: 0;
+ margin-inline: calc(-1 * #{$item-padding-inline});
+ padding-block: 0;
+ padding-inline: 0;
+
+ &-collapsed {
+ margin-block: awsui.$space-scaled-xxxl;
+ margin-inline: 0;
+ padding-inline-start: 0;
+ padding-inline-end: 0;
+ }
}
+// ==========================================================================
+// List items
+// ==========================================================================
.list-item {
- margin-block: awsui.$space-scaled-xs;
margin-inline: 0;
+ margin-block: $item-margin-block;
padding-block: 0;
- padding-inline: 0;
+ padding-inline: $item-padding-inline;
list-style: none;
-
+ @include styles.with-motion {
+ transition: margin awsui.$motion-duration-expressive awsui.$motion-easing-responsive;
+ }
// Remove margin from first item in side nav, outer block margins are covered by list-container
.list-variant-root--first > &:first-child {
margin-block-start: 0px;
}
+
+ &--collapsed {
+ margin-block: $item-collapsed-gap;
+ display: flex;
+ justify-content: center;
+ padding-block: 0;
+ padding-inline: 0;
+ @include styles.with-motion {
+ transition: margin awsui.$motion-duration-expressive awsui.$motion-easing-responsive;
+ }
+ }
}
+// ==========================================================================
+// Sections and expandable link groups (InternalExpandableSection)
+// ==========================================================================
.section,
.expandable-link-group {
- margin-inline-start: calc(-1 * #{awsui.$space-l});
+ margin-inline-start: calc(-1 * #{$expandable-icon-negative-margin});
+ /* stylelint-disable-next-line selector-max-type */
+ & > div {
+ padding-block: 0;
+ padding-inline: 0;
+ /* stylelint-disable-next-line selector-max-type */
+ > :first-child {
+ display: grid;
+ align-items: flex-start;
+ padding-block: calc((#{$item-height} - #{awsui.$line-height-body-m}) / 2);
+ }
+ }
- &--no-ident {
+ &--no-ident,
+ &--expand-icon-end {
margin-inline-start: 0;
}
}
+.expandable-link-group > div:first-child {
+ @include item-radius;
+ display: flex;
+ padding-inline: $item-padding-inline;
+ margin-inline: calc(-1 * #{$item-padding-inline});
+}
+
+.expandable-link-group--expand-icon-end {
+ margin-inline-end: calc(-2 * #{$item-padding-inline});
+}
+
+.expandable-link-group-active > div:first-child {
+ background-color: color-mix(in srgb, #{awsui.$color-item-selected} 8%, transparent);
+}
+
.section {
margin-block: calc(#{awsui.$space-scaled-2x-l} - #{awsui.$border-divider-section-width});
&.refresh {
- margin-block: calc(#{awsui.$space-scaled-2x-m} - #{awsui.$border-divider-section-width});
+ margin-block: calc(#{awsui.$space-scaled-s} - #{awsui.$border-divider-section-width});
}
// Remove margin from section if it is the first item in side nav to prevent double margin stacking
.list-variant-root--first > .list-item:first-child > & {
margin-block-start: 0px;
}
- // HACK: Remove padding from section header and content to rely on margin collapsing rules.
- /* stylelint-disable-next-line selector-max-type */
- > div {
- padding-block: 0;
- padding-inline: 0;
- }
}
-.list-variant-section-group {
- margin-block: 0;
+// ==========================================================================
+// Section groups
+// ==========================================================================
+.section-group {
+ @include styles.font-heading-s;
+ margin-block: calc(#{awsui.$space-scaled-2x-l} - #{awsui.$border-divider-section-width});
margin-inline: 0;
- padding-block: 0;
- padding-inline: 0;
+
+ .list-variant-root--first > .list-item:first-child > & {
+ margin-block-start: 0px;
+ }
}
-.section-group {
- @include styles.font-heading-m;
+.section-group-title {
+ display: flex;
+ align-items: center;
margin-block: 0;
- margin-inline: 0;
+ padding-block: awsui.$space-scaled-xxs;
+ &--eyebrow {
+ @include styles.font-body-s;
+ text-transform: uppercase;
+ letter-spacing: 0.35px;
+ color: awsui.$color-text-body-secondary;
+ }
}
-.section-group-title {
- /* used in test-utils */
+.section-header-text {
+ display: inline-flex;
}
+// ==========================================================================
+// Links — all links use $item-height for consistent row sizing and collapse icon size.
+// ==========================================================================
.link {
@include styles.font-body-m;
color: awsui.$color-text-body-secondary;
+ display: inline-flex;
+ padding-block: calc((#{$item-height} - #{awsui.$line-height-body-m}) / 2);
+ min-inline-size: $item-height;
+ padding-inline: $item-padding-inline;
+ margin-inline: calc(-1 * #{$item-padding-inline});
+ align-items: flex-start;
+ @include item-radius;
font-weight: styles.$font-weight-normal;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
-}
+ @include styles.with-motion {
+ transition:
+ background-color awsui.$motion-duration-expressive awsui.$motion-easing-responsive,
+ color awsui.$motion-duration-expressive awsui.$motion-easing-expressive;
+ }
-.link-active {
- font-weight: awsui.$font-wayfinding-link-active-weight;
- @include styles.font-smoothing;
- color: awsui.$color-text-accent;
+ &--active {
+ font-weight: awsui.$font-wayfinding-link-active-weight;
+ @include styles.font-smoothing;
+ color: awsui.$color-text-accent;
+ &.link--pill {
+ background-color: color-mix(in srgb, #{awsui.$color-item-selected} 8%, transparent);
+ color: awsui.$color-item-selected;
+ }
+ }
+
+ &--collapsed {
+ padding-inline: 0;
+ margin-inline: 0;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &--pill {
+ display: flex;
+ }
}
.header-link,
@@ -182,10 +339,21 @@
}
}
+// ==========================================================================
+// Misc
+// ==========================================================================
.info {
margin-inline-start: awsui.$space-xs;
}
+.item-icon {
+ display: inline-flex;
+ flex-shrink: 0;
+ &:not(.item-icon-collapsed) {
+ margin-inline-end: $item-icon-text-gap;
+ }
+}
+
.external-icon {
margin-inline-start: awsui.$space-xxs;
}
@@ -193,18 +361,19 @@
.divider {
border-block: none;
border-inline: none;
-}
-
-.divider-default {
- margin-block: awsui.$space-scaled-2x-xl;
- margin-inline: calc(-1 * #{awsui.$space-panel-divider-margin-horizontal});
- border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-divider-default;
-}
-
-.divider-header {
- margin-block: 0;
- border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-panel-header;
- .with-toolbar > & {
- border-color: transparent;
+ &-default {
+ margin-block: awsui.$space-scaled-2x-xl;
+ margin-inline: calc(-1 * #{awsui.$space-panel-divider-margin-horizontal});
+ border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-divider-default;
+ &.divider-collapsed {
+ margin-block: awsui.$space-scaled-2x-m;
+ }
+ }
+ &-header {
+ margin-block: 0;
+ border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-panel-header;
+ .with-toolbar > & {
+ border-color: transparent;
+ }
}
}
diff --git a/src/side-navigation/test-classes/styles.scss b/src/side-navigation/test-classes/styles.scss
index 63ac853f7b..7f06e59e0e 100644
--- a/src/side-navigation/test-classes/styles.scss
+++ b/src/side-navigation/test-classes/styles.scss
@@ -5,3 +5,7 @@
.info {
/* used in test-utils */
}
+
+.item-icon {
+ /* used in test-utils */
+}
diff --git a/style-dictionary/utils/token-names.ts b/style-dictionary/utils/token-names.ts
index 35b14f9a09..3fd56cf707 100644
--- a/style-dictionary/utils/token-names.ts
+++ b/style-dictionary/utils/token-names.ts
@@ -1057,7 +1057,8 @@ export type SizesTokenName =
| 'sizeIconNormal'
| 'sizeTableSelectionHorizontal'
| 'sizeVerticalInput'
- | 'sizeVerticalPanelIconOffset';
+ | 'sizeVerticalPanelIconOffset'
+ | 'sizeSideNavigationItemHeight';
export type SpacingTokenName =
| 'spaceAlertActionLeft'
| 'spaceAlertHorizontal'
@@ -1130,6 +1131,8 @@ export type SpacingTokenName =
| 'spaceTableHorizontal'
| 'spaceTileGutter'
| 'spaceTreeViewIndentation'
+ | 'spaceSideNavigationItemGap'
+ | 'spaceSideNavigationItemCollapsedGap'
| 'spaceL'
| 'spaceM'
| 'spaceNone'
diff --git a/style-dictionary/visual-refresh/metadata/colors.ts b/style-dictionary/visual-refresh/metadata/colors.ts
index e583f0039e..c2cd227abd 100644
--- a/style-dictionary/visual-refresh/metadata/colors.ts
+++ b/style-dictionary/visual-refresh/metadata/colors.ts
@@ -168,6 +168,8 @@ const metadata: StyleDictionary.MetadataIndex = {
colorBackgroundLayoutPanelContent: {
description:
'The background color of app layout panel content area. For example: The side navigation and tools panel content background color.',
+ public: true,
+ themeable: true,
},
colorBackgroundLayoutToolbar: {
description:
diff --git a/style-dictionary/visual-refresh/metadata/sizes.ts b/style-dictionary/visual-refresh/metadata/sizes.ts
index 5231424e5b..f4e41b8d7e 100644
--- a/style-dictionary/visual-refresh/metadata/sizes.ts
+++ b/style-dictionary/visual-refresh/metadata/sizes.ts
@@ -9,6 +9,11 @@ const metadata: StyleDictionary.MetadataIndex = {
public: true,
themeable: true,
},
+ sizeSideNavigationItemHeight: {
+ description: 'The minimum height of side navigation items.',
+ public: true,
+ themeable: true,
+ },
};
export default metadata;
diff --git a/style-dictionary/visual-refresh/metadata/spacing.ts b/style-dictionary/visual-refresh/metadata/spacing.ts
index 8b68212714..02a8569c40 100644
--- a/style-dictionary/visual-refresh/metadata/spacing.ts
+++ b/style-dictionary/visual-refresh/metadata/spacing.ts
@@ -174,6 +174,16 @@ const metadata: StyleDictionary.MetadataIndex = {
public: true,
themeable: false,
},
+ spaceSideNavigationItemGap: {
+ description: 'The vertical gap between side navigation items.',
+ public: true,
+ themeable: true,
+ },
+ spaceSideNavigationItemCollapsedGap: {
+ description: 'The vertical gap between side navigation items in collapsed state.',
+ public: true,
+ themeable: true,
+ },
spaceButtonHorizontal: {
description: 'The horizontal padding inside buttons.',
public: true,
diff --git a/style-dictionary/visual-refresh/sizes.ts b/style-dictionary/visual-refresh/sizes.ts
index d80230db33..ccf991f19b 100644
--- a/style-dictionary/visual-refresh/sizes.ts
+++ b/style-dictionary/visual-refresh/sizes.ts
@@ -13,6 +13,7 @@ const tokens: StyleDictionary.SizesDictionary = {
sizeTableSelectionHorizontal: '40px',
sizeVerticalInput: { comfortable: '32px', compact: '28px' },
sizeVerticalPanelIconOffset: { comfortable: '15px', compact: '13px' },
+ sizeSideNavigationItemHeight: '28px',
};
const expandedTokens: StyleDictionary.ExpandedDensityScopeDictionary = expandDensityDictionary(tokens);
diff --git a/style-dictionary/visual-refresh/spacing.ts b/style-dictionary/visual-refresh/spacing.ts
index 2dae31ea8b..0e37387a4c 100644
--- a/style-dictionary/visual-refresh/spacing.ts
+++ b/style-dictionary/visual-refresh/spacing.ts
@@ -52,7 +52,7 @@ const tokens: StyleDictionary.SpacingDictionary = {
spaceLayoutTogglePadding: '{spaceStaticS}',
spaceModalContentBottom: '{spaceScaled2xM}',
spaceModalHorizontal: '{spaceContainerHorizontal}',
- spacePanelContentBottom: '{spaceScaledXxxl}',
+ spacePanelContentBottom: '{spaceScaledL}',
spacePanelContentTop: '{spaceScaledL}',
spacePanelDividerMarginHorizontal: '{spaceXs}',
spacePanelHeaderVertical: '{spaceScaledL}',
@@ -74,6 +74,8 @@ const tokens: StyleDictionary.SpacingDictionary = {
spaceTableHeaderToolsFullPageBottom: '4px',
spaceTableHorizontal: '{spaceContainerHorizontal}',
spaceTreeViewIndentation: '{spaceXl}',
+ spaceSideNavigationItemGap: '{spaceScaledXxs}',
+ spaceSideNavigationItemCollapsedGap: '{spaceXs}',
spaceTileGutter: { comfortable: '{spaceXl}', compact: '{spaceM}' },
spaceActionCardHorizontalDefault: '{spaceCardHorizontalDefault}',
spaceActionCardHorizontalEmbedded: '{spaceCardHorizontalEmbedded}',