← All proposals

Menu Button

Starting point for a MenuButton proposal.

Extends a PDS componentOpen in Storybook

proposals/MenuButton/menu-button.css237 lines · View on GitHub
.pds-menu-button {
	display: inline-block;
	position: relative;
}

button.pds-menu-button__trigger .pds-button__icon {
	position: relative;
	transition: var(--pds-animation-transition-rotation);
}

button.pds-menu-button__trigger[aria-expanded='true'] .pds-button__icon {
	transform: rotate(180deg) translateY(-1px);
	transition: var(--pds-animation-transition-rotation);
}

/* Subtle variant (formerly navbar) */
button.pds-button--subtle.pds-menu-button__trigger {
	gap: var(--pds-spacing-2xs);

	.pds-button__icon {
		color: var(--pds-color-foreground-default-secondary);
	}
}

button.pds-button--subtle.pds-menu-button__trigger[aria-expanded='true'],
button.pds-button--subtle.pds-menu-button__trigger[aria-expanded='true']:active {
	background-color: var(--pds-color-button-subtle-background-active);
	border-color: var(--pds-color-button-subtle-background-active);
	color: var(--pds-color-foreground-default-secondary);
}

/* navbar variant: same as subtle (hover/active/foreground all inherited via
   the subtle button variant above) except no background at rest — only on
   hover or while the menu is expanded. */
button.pds-button--subtle.pds-menu-button__trigger.pds-menu-button__trigger--navbar {
	--button-background-color: transparent;
}

/* Icon only */
button.pds-menu-button__trigger--icon-only[aria-expanded='true'],
button.pds-menu-button__trigger--icon-only[aria-expanded='true']:active {
	background-color: var(--pds-color-interactive-background-active);
	color: var(--pds-color-foreground-default);
}

/* Icon only + agentic: the expanded rule above paints a flat background-color,
   which the gradient-border button's fill layers sit on top of. Use its fill
   variable so the open state still shows. */
button.pds-menu-button__trigger--icon-only.pds-icon-button--agentic[aria-expanded='true'],
button.pds-menu-button__trigger--icon-only.pds-icon-button--agentic[aria-expanded='true']:active {
	--icon-button-fill: var(--pds-color-button-secondary-background-active);

	background-color: transparent;
}

/* Dropdown menu — pds-dropdown__panel provides: background, border-radius, box-shadow, z-index.
   The gap between trigger and panel is applied via Floating UI's offset() (see useDropdown
   offsetValue) so it stays on the far side of the trigger whether the menu opens down or flips up. */
.pds-menu-button__listbox {
	max-width: 30rem;
	width: max-content;
}

ul.pds-menu-button__listbox {
	margin-inline: 0;
}

.pds-menu-button__listbox:focus-visible {
	outline: none;
}

.pds-menu-button ul,
.pds-menu-button ul [role='separator'] {
	list-style: none;
}

.pds-menu-button ul:not(.pds-menu-button__listbox) {
	margin: 0;
	padding: 0;
}

/* Grouped sub-lists live inside the desktop panel, which renders in a portal
   (outside .pds-menu-button). Reset them via the panel class so the rule
   travels with the portaled panel. */
.pds-menu-button__listbox ul {
	list-style: none;
	margin: 0;
	padding: 0;
}

/* pds-dropdown__heading provides: heading styles */
/* pds-dropdown__separator provides: separator border */
/* pds-dropdown__item provides: item base styles (color, cursor, padding, hover, focus, active) */
/* pds-dropdown__item--focused / --critical / --disabled provide modifier states */

.pds-menu-button__listbox .pds-dropdown__separator {
	margin-block-end: var(--pds-spacing-4xs);
}

/* Accordion label wrapper — keeps label text and tally together as one flex item */
.pds-menu-button__accordion-label {
	align-items: center;
	display: inline-flex;
	gap: var(--pds-spacing-xs);
}

/* Extra breathing room around tally in menu button trigger */
.pds-menu-button__trigger .pds-button__tally {
	margin-inline: var(--pds-spacing-5xs);
}

/* Extra breathing room between status dot and label in menu button trigger */
.pds-menu-button__trigger .pds-status-dot {
	margin-inline-end: var(--pds-spacing-5xs);
}

/* Menu item label (icon + text, not description) */
.pds-menu-button__item-label {
	align-items: center;
	display: flex;
	gap: var(--pds-spacing-xs);
}

/* Link items: fill the flex row of pds-dropdown__item */
.pds-menu-button__listbox .pds-dropdown__item > a,
[data-pds-mobile] .pds-navbar__mobile .pds-menu-button .pds-dropdown__item > a {
	color: inherit;
	flex: 1;
	text-decoration: none;
}

.pds-menu-button__listbox [role='menuitem'][aria-disabled='true'] {
	background: transparent;
	cursor: not-allowed;
	opacity: 0.5;
}

/* Mobile accordion — hidden by default, shown inside [data-pds-mobile] .pds-navbar__mobile */
.pds-menu-button .pds-nav-accordion {
	display: none;
}

[data-pds-mobile] .pds-navbar__mobile .pds-menu-button .pds-nav-accordion {
	display: block;
}

/* Hide Floating UI trigger inside the mobile navbar panel -- excludes a
   MenuButton nested inside RowActionsMenu (e.g. BookmarksMenu's per-row
   kebab), which isn't the navbar item itself and needs its own trigger. */
[data-pds-mobile]
	.pds-navbar__mobile
	.pds-menu-button__trigger:not(
		.pds-row-actions-menu .pds-menu-button__trigger
	) {
	display: none;
}

/* Mobile accordion toggle: match workspace selector typography and padding */
[data-pds-mobile]
	.pds-navbar__mobile
	.pds-menu-button
	.pds-nav-accordion__toggle {
	font-size: var(--pds-typography-size-m);
	min-height: unset;
	padding-block: var(--pds-spacing-m);
	padding-inline: var(--pds-spacing-m);
}

/* Full-bleed accordion wrapper inside the mobile navbar panel -- excludes a
   MenuButton nested inside RowActionsMenu (see the trigger rule above). */
[data-pds-mobile]
	.pds-navbar__mobile
	.pds-menu-button:not(.pds-row-actions-menu .pds-menu-button) {
	background-color: var(--pds-color-surface-default);
	border-block-end: var(--pds-border-width-default) solid
		var(--pds-color-border-default);
	left: 50%;
	margin-inline-start: -50vw;
	position: relative;
	right: 50%;
	width: 100vw;
}

/* Mobile accordion menu items */
[data-pds-mobile] .pds-navbar__mobile .pds-menu-button__mobile-menu {
	padding: 0;
}

[data-pds-mobile] .pds-navbar__mobile .pds-menu-button__mobile-menu li {
	border-radius: 0;
	font-size: var(--pds-typography-size-m);
	padding-block: var(--pds-spacing-m);
	padding-inline: var(--pds-spacing-m);

	&[role='menuitem']:hover {
		background-color: var(--pds-color-dropdown-item-background-hover);
	}

	&[role='separator'] {
		background-color: var(--pds-color-border-default);
		height: var(--pds-border-width-default);
		margin-block: 0;
		margin-inline: var(--pds-spacing-m);
		padding: 0;
	}
}

[data-pds-mobile]
	.pds-navbar__mobile
	.pds-menu-button
	.pds-menu-button__listbox-heading {
	color: var(--pds-color-dropdown-heading);
	font-size: var(--pds-typography-size-s);
}

/* Icon-label layout for icon-only display type in accordion */
.pds-menu-button__icon-label {
	align-items: center;
	display: flex;
	gap: var(--pds-spacing-s);

	.pds-icon {
		min-width: var(--pds-spacing-xl);
	}
}

/* Selected item (`isSelected`): the check sits at the end of the row. Unselected
   items keep it hidden but in the layout, so the panel width is stable. */
.pds-menu-button__item-check {
	flex-shrink: 0;
	margin-inline-start: auto;
}

.pds-menu-button__item-check--hidden {
	visibility: hidden;
}