← All proposals

Button

Starting point for a Button proposal.

Extends a PDS componentOpen in Storybook

proposals/Button/button.css438 lines · View on GitHub
.pds-button {
	--button-font-size: var(--pds-typography-size-s);
	--button-height: var(--pds-spacing-button-height-s);
	--button-padding-inline: var(--pds-spacing-button-padding-inline-s);
	--button-gap-inline: var(--pds-spacing-button-gap-inline-s);
	--button-background-color: var(--pds-color-button-primary-background-default);
	--button-background-color-hover: var(
		--pds-color-button-primary-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-primary-background-active
	);
	--button-foreground-color: var(--pds-color-button-primary-foreground);
	--button-foreground-color-hover: var(--button-foreground-color);
	--button-border-color: var(--pds-color-button-primary-background-default);
	--button-border-color-hover: var(--pds-color-button-primary-background-hover);
	--button-border-color-active: var(--button-background-color-active);
	--button-box-shadow-active: var(--pds-elevation-button-primary-active);

	align-items: center;
	background-color: var(--button-background-color);
	border-color: var(--button-border-color);
	border-radius: var(--pds-border-radius-button);
	border-style: solid;
	border-width: var(--pds-border-width-default);
	color: var(--button-foreground-color);
	cursor: pointer;
	display: inline-flex;
	font-family: var(--pds-typography-ff-default);
	font-size: var(--button-font-size);
	font-weight: var(--pds-typography-fw-medium);
	gap: var(--button-gap-inline);
	height: var(--button-height);
	justify-content: center;
	line-height: 1;
	max-height: var(--button-height);
	padding-block: 0;
	padding-inline: var(--button-padding-inline);
	text-decoration: none;
	transition: var(--pds-animation-transition-button);
	white-space: nowrap;
}

/* A working-indicator as the first child sits slightly looser than an icon
   would at the same spot — tighten the leading edge and open up the gap to
   the label so it doesn't read as crowding it. */
.pds-button:has(> .pds-button__leading > .pds-button__working-indicator) {
	padding-inline-start: calc(
		var(--button-padding-inline) - var(--pds-spacing-4xs)
	);
}

.pds-button__working-indicator {
	margin-inline-end: var(--pds-spacing-4xs);
}

/* Holds the working spinner and leading icon/status dot without adding a box,
   so its children are still direct flex items of the button. */
.pds-button__leading {
	display: contents;
}

.pds-button:disabled {
	cursor: not-allowed;
	opacity: 40%;
}

.pds-button:hover:enabled,
a.pds-button:hover {
	--button-background-color: var(--button-background-color-hover);
	--button-border-color: var(--button-border-color-hover);
	color: var(--button-foreground-color-hover);
	text-decoration: none;
}

.pds-button:active:enabled,
a.pds-button:active {
	--button-background-color: var(--button-background-color-active);
	--button-border-color: var(--button-border-color-active);
	box-shadow: var(--button-box-shadow-active);
	color: var(--button-foreground-color-hover);
	text-decoration: none;
}

.pds-button:focus-visible {
	outline: var(--pds-border-width-outline) solid
		var(--pds-color-interactive-focus);
	outline-offset: 1px;
	transition: var(--pds-animation-transition-focus);
}

.pds-button__icon {
	flex-shrink: 0;
}

.pds-button--l .pds-button__icon:not(.pds-status-dot) {
	height: var(--button-font-size);
	width: var(--button-font-size);
}

.pds-button__tally {
	flex-shrink: 0;
}

/* Tally ring — lifts badge off filled button backgrounds */
.pds-button--primary .pds-button__tally,
.pds-button--highlight .pds-button__tally {
	box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.75);
}

/* Status dot — scale with button size */
.pds-button--xs .pds-status-dot {
	--dot-size: 8px;
}
.pds-button--s .pds-status-dot {
	--dot-size: 10px;
}
.pds-button--m .pds-status-dot {
	--dot-size: 12px;
	margin-inline-end: var(--pds-spacing-5xs);
}
.pds-button--l .pds-status-dot {
	--dot-size: 14px;
	margin-inline-end: var(--pds-spacing-4xs);
}

/* Full Width Button */
.pds-button--full-width {
	width: 100%;
}

/* Button Sizes */
.pds-button--xs {
	--button-font-size: var(--pds-typography-size-xs);
	--button-height: var(--pds-spacing-button-height-xs);
	--button-padding-inline: var(--pds-spacing-button-padding-inline-xs);
	--button-gap-inline: var(--pds-spacing-button-gap-inline-xs);
}
.pds-button--s {
	--button-font-size: var(--pds-typography-size-s);
	--button-height: var(--pds-spacing-button-height-s);
	--button-padding-inline: var(--pds-spacing-button-padding-inline-s);
	-button-gap-inline: var(--pds-spacing-button-gap-inline-s);
}
.pds-button--m {
	--button-font-size: var(--pds-typography-size-m);
	--button-height: var(--pds-spacing-button-height-m);
	--button-padding-inline: var(--pds-spacing-button-padding-inline-m);
	--button-gap-inline: var(--pds-spacing-button-gap-inline-m);
}
.pds-button--l {
	--button-font-size: var(--pds-typography-size-l);
	--button-height: var(--pds-spacing-button-height-l);
	--button-padding-inline: var(--pds-spacing-button-padding-inline-l);
	--button-gap-inline: var(--pds-spacing-button-gap-inline-l);
}

/* Button Variants */
.pds-button--secondary {
	--button-background-color: var(
		--pds-color-button-secondary-background-default
	);
	--button-background-color-hover: var(
		--pds-color-button-secondary-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-secondary-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-secondary-active);
	--button-foreground-color: var(
		--pds-color-button-secondary-foreground-default
	);
	--button-foreground-color-hover: var(
		--pds-color-button-secondary-foreground-hover
	);
	--button-border-color: var(--pds-color-button-secondary-border);
	--button-border-color-hover: var(--pds-color-button-secondary-border);
	--button-border-color-active: var(--pds-color-button-secondary-border);
}
.pds-button--subtle {
	--button-background-color: var(--pds-color-button-subtle-background-default);
	--button-background-color-hover: var(
		--pds-color-button-subtle-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-subtle-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-subtle-active);
	--button-foreground-color: var(--pds-color-button-subtle-foreground);
	/*
	 * Literal `transparent`, not a reference to the background token.
	 * The background tokens are alpha overlays so subtle reads correctly
	 * on any surface color — but border-color paints as its own stroke
	 * on top of the background fill, so aliasing the same translucent
	 * value here would double-composite in the border ring and show up
	 * as a visible seam, even at matching nominal color. `transparent`
	 * composites as nothing regardless of what's underneath, so the
	 * border occupies its width (keeping sizing consistent with every
	 * other variant) without ever painting.
	 */
	--button-border-color: transparent;
	--button-border-color-hover: transparent;
	--button-border-color-active: transparent;
}
.pds-button--critical {
	--button-background-color: var(
		--pds-color-button-critical-background-default
	);
	--button-background-color-hover: var(
		--pds-color-button-critical-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-critical-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-critical-active);
	--button-foreground-color: var(--pds-color-button-critical-foreground);
	--button-border-color: var(--pds-color-button-critical-background-default);
	--button-border-color-hover: var(
		--pds-color-button-critical-background-hover
	);
	--button-border-color-active: var(--button-background-color-active);
}
.pds-button--critical-secondary {
	--button-background-color: var(
		--pds-color-button-critical-secondary-background-default
	);
	--button-background-color-hover: var(
		--pds-color-button-critical-secondary-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-critical-secondary-background-active
	);
	--button-box-shadow-active: var(
		--pds-elevation-button-critical-secondary-active
	);
	--button-foreground-color: var(
		--pds-color-button-critical-secondary-foreground
	);
	--button-border-color: var(
		--pds-color-button-critical-secondary-border-default
	);
	--button-border-color-hover: var(
		--pds-color-button-critical-secondary-border-default
	);
	--button-border-color-active: var(
		--pds-color-button-critical-secondary-border-default
	);
}
.pds-button--brand {
	--button-background-color: var(--pds-color-button-brand-background-default);
	--button-background-color-hover: var(
		--pds-color-button-brand-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-brand-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-brand-active);
	--button-foreground-color: var(--pds-color-button-brand-foreground);
	--button-border-color: var(--pds-color-button-brand-background-default);
	--button-border-color-hover: var(--pds-color-button-brand-background-hover);
	--button-border-color-active: var(--button-background-color-active);
}
.pds-button--highlight {
	--button-background-color: var(
		--pds-color-button-highlight-background-default
	);
	--button-background-color-hover: var(
		--pds-color-button-highlight-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-highlight-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-highlight-active);
	--button-foreground-color: var(--pds-color-button-highlight-foreground);
	--button-border-color: var(--pds-color-button-highlight-background-default);
	--button-border-color-hover: var(
		--pds-color-button-highlight-background-hover
	);
	--button-border-color-active: var(--button-background-color-active);
}
.pds-button--reverse {
	--button-background-color: var(--pds-color-button-reverse-background-default);
	--button-background-color-hover: var(
		--pds-color-button-reverse-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-reverse-background-active
	);
	--button-box-shadow-active: var(--pds-elevation-button-reverse-active);
	--button-foreground-color: var(--pds-color-button-reverse-foreground);
	--button-border-color: var(--pds-color-button-reverse-background-default);
	--button-border-color-hover: var(--pds-color-button-reverse-background-hover);
	--button-border-color-active: var(--button-background-color-active);
}
.pds-button--reverse-secondary {
	--button-background-color: var(
		--pds-color-button-reverse-secondary-background-default
	);
	--button-background-color-hover: var(
		--pds-color-button-reverse-secondary-background-hover
	);
	--button-background-color-active: var(
		--pds-color-button-reverse-secondary-background-active
	);
	--button-box-shadow-active: var(
		--pds-elevation-button-reverse-secondary-active
	);
	--button-foreground-color: var(
		--pds-color-button-reverse-secondary-foreground
	);
	--button-border-color: var(--pds-color-button-reverse-secondary-border);
	--button-border-color-hover: var(--pds-color-button-reverse-secondary-border);
	--button-border-color-active: var(
		--pds-color-button-reverse-secondary-border
	);
}

/* Agentic modifier (primary and secondary): the AI gradient as the border.
   --pds-gradient-ai-active is a gradient, not a color, so it cannot go in
   border-color. The border is transparent and the gradient is painted on the
   border-box underneath; the fill layers sit on the padding-box above it. The
   secondary background tokens are translucent, so an opaque surface layer
   keeps the gradient from showing through the middle of the button. */
.pds-button--agentic {
	--button-border-color: transparent;
	--button-border-color-hover: transparent;
	--button-border-color-active: transparent;

	background-clip: padding-box, padding-box, border-box;
	background-color: transparent;
	background-image:
		linear-gradient(
			var(--button-background-color),
			var(--button-background-color)
		),
		linear-gradient(
			var(--pds-color-surface-default),
			var(--pds-color-surface-default)
		),
		var(--pds-gradient-ai-active);
	background-origin: border-box;
}

/* Special Variants */
.pds-button--link {
	background-color: transparent;
	border-color: transparent;
	border: none;
	border-radius: 0;
	color: var(--pds-color-interactive-link-default);
	height: unset;
	justify-content: unset;
	letter-spacing: 0;
	max-height: unset;
	padding-inline: 0;
	padding: 0;
	transition: var(--pds-animation-transition-button);
}
.pds-button--link:hover:enabled,
a.pds-button--link:hover {
	color: var(--pds-color-interactive-link-hover);
	text-decoration: underline;
}

.pds-button--link:active:enabled,
a.pds-button--link:active {
	box-shadow: none;
	color: var(--pds-color-interactive-link-active);
}

/* The link variant has no button container (background/border/padding) to
   visually balance the icon against, unlike every other variant -- a
   full icon-size step down reads as too small, so nudge it down with a
   scale instead. Covers both Button's own icon and ButtonLink's cloned one. */
.pds-button--link .pds-button__icon,
.pds-button--link .pds-button-link__icon {
	transform: scale(0.9);
}

/* Loading State */
.pds-button--loading {
	cursor: not-allowed;

	&:hover:enabled,
	&:active {
		--button-background-color: var(--pulse-regular-base-color);
		--button-border-color: var(--pulse-regular-base-color);
	}
}

.pds-button--loading.pds-pulse {
	background-color: var(--pulse-regular-base-color);
	background-image: none;
	border-color: var(--pulse-regular-base-color);
	color: transparent;
}

.pds-button--loading .pds-button__icon,
.pds-button--loading .pds-button__tally,
.pds-button--loading .pds-button__working-indicator {
	opacity: 0;
}

/* Button Group */
.pds-button-group {
	display: flex;
	flex-direction: column;
	row-gap: var(--pds-spacing-m);
	width: 100%;
}

/* Opts out of the default mobile stack-to-full-width behavior above, for
   groups used as a compact toolbar (e.g. next to a heading) rather than a
   set of stacked primary/secondary actions. Wraps instead of overflowing
   when there isn't room to stay on one line (e.g. a split button's menu
   alongside another action at narrow widths). */
.pds-button-group--inline {
	column-gap: var(--pds-spacing-s);
	flex-direction: row;
	flex-wrap: wrap;
	row-gap: var(--pds-spacing-s);
	width: unset;
}

/* --pds-bp-m. This repo has no postcss custom-media plugin, so the literal
   value is used here; the port restores `(--pds-bp-m)`. */
@media (min-width: 768px) {
	.pds-button-group {
		column-gap: var(--pds-spacing-s);
		flex-direction: row;
		width: unset;
	}

	.pds-button-group--l {
		column-gap: var(--pds-spacing-m);
	}
}