From 5645c69826aa4b89ae048dba7ec75945fb1499de Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Mon, 15 Dec 2025 15:54:20 -0800 Subject: [PATCH] Button cleanup (#41928) --- .bundlewatch.config.json | 2 +- scss/_variables.scss | 9 - scss/buttons/_button-group.scss | 32 +- scss/buttons/_button-variables.scss | 36 +- scss/buttons/_button.scss | 341 +++++++++++----- .../shortcodes/ButtonPlayground.astro | 329 ++++++++++++++++ .../content/docs/components/button-group.mdx | 58 +-- site/src/content/docs/components/buttons.mdx | 366 +++++++++++------- .../content/docs/getting-started/approach.mdx | 4 +- site/src/content/docs/guides/quickstart.mdx | 2 +- site/src/content/docs/migration.mdx | 2 +- site/src/scss/_component-examples.scss | 5 - site/src/scss/_details.scss | 2 + site/src/types/auto-import.d.ts | 1 + 14 files changed, 866 insertions(+), 323 deletions(-) create mode 100644 site/src/components/shortcodes/ButtonPlayground.astro diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index b8e0eff168..3301f9a253 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -26,7 +26,7 @@ }, { "path": "./dist/css/bootstrap.css", - "maxSize": "37.5 kB" + "maxSize": "37.75 kB" }, { "path": "./dist/css/bootstrap.min.css", diff --git a/scss/_variables.scss b/scss/_variables.scss index a3abd350f1..7a657e2f0c 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -414,15 +414,6 @@ $btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; $btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; $btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$btn-hover-bg-shade-amount: 15% !default; -$btn-hover-bg-tint-amount: 15% !default; -$btn-hover-border-shade-amount: 20% !default; -$btn-hover-border-tint-amount: 10% !default; -$btn-active-bg-shade-amount: 20% !default; -$btn-active-bg-tint-amount: 20% !default; -$btn-active-border-shade-amount: 25% !default; -$btn-active-border-tint-amount: 10% !default; // scss-docs-end btn-variables diff --git a/scss/buttons/_button-group.scss b/scss/buttons/_button-group.scss index c2dc7c3560..07cc64efb9 100644 --- a/scss/buttons/_button-group.scss +++ b/scss/buttons/_button-group.scss @@ -12,15 +12,16 @@ display: inline-flex; vertical-align: middle; // match .btn alignment given font-size hack above - > .btn { + > .btn, + > .btn-check { position: relative; flex: 1 1 auto; } // Bring the hover, focused, and "active" buttons to the front to overlay // the borders properly - > .btn-check:checked + .btn, - > .btn-check:focus + .btn, + > .btn-check:has(input:checked), + > .btn-check:has(input:focus), > .btn:hover, > .btn:focus, > .btn:active, @@ -44,24 +45,23 @@ @include border-radius($btn-border-radius); // Prevent double borders when buttons are next to each other - > :not(.btn-check:first-child) + .btn, + > .btn:not(:first-child), + > .btn-check:not(:first-child), > .btn-group:not(:first-child) { margin-inline-start: calc(-1 * #{$btn-border-width}); } // Reset rounded corners > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-check:not(:last-child), > .btn.dropdown-toggle-split:first-child, > .btn-group:not(:last-child) > .btn { @include border-end-radius(0); } - // The left radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, + // The left radius should be 0 if the button is not the first child + > .btn:not(:first-child), + > .btn-check:not(:first-child), > .btn-group:not(:first-child) > .btn { @include border-start-radius(0); } @@ -124,27 +124,27 @@ justify-content: center; > .btn, + > .btn-check, > .btn-group { width: 100%; } > .btn:not(:first-child), + > .btn-check:not(:first-child), > .btn-group:not(:first-child) { margin-top: calc(-1 * #{$btn-border-width}); } // Reset rounded corners > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-check:not(:last-child), > .btn-group:not(:last-child) > .btn { @include border-bottom-radius(0); } - // The top radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, + // The top radius should be 0 if the button is not the first child + > .btn:not(:first-child), + > .btn-check:not(:first-child), > .btn-group:not(:first-child) > .btn { @include border-top-radius(0); } diff --git a/scss/buttons/_button-variables.scss b/scss/buttons/_button-variables.scss index 1b682fcdab..b72521e4e5 100644 --- a/scss/buttons/_button-variables.scss +++ b/scss/buttons/_button-variables.scss @@ -6,7 +6,7 @@ @use "../forms/form-variables" as *; // scss-docs-start btn-variables -$btn-color: var(--#{$prefix}color-body) !default; +$btn-color: var(--#{$prefix}fg-body) !default; $btn-padding-y: $input-btn-padding-y !default; $btn-padding-x: $input-btn-padding-x !default; $btn-font-family: $input-btn-font-family !default; @@ -14,42 +14,44 @@ $btn-font-size: $input-btn-font-size !default; $btn-line-height: $input-btn-line-height !default; $btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping +$btn-padding-y-xs: .125rem !default; +$btn-padding-x-xs: .375rem !default; +$btn-font-size-xs: var(--#{$prefix}font-size-xs) !default; +$btn-line-height-xs: 1.125rem !default; + $btn-padding-y-sm: $input-btn-padding-y-sm !default; $btn-padding-x-sm: $input-btn-padding-x-sm !default; -$btn-font-size-sm: $input-btn-font-size-sm !default; +$btn-font-size-sm: var(--#{$prefix}font-size-sm) !default; +$btn-line-height-sm: 1.125rem !default; $btn-padding-y-lg: $input-btn-padding-y-lg !default; $btn-padding-x-lg: $input-btn-padding-x-lg !default; -$btn-font-size-lg: $input-btn-font-size-lg !default; +$btn-font-size-lg: 16px !default; +$btn-line-height-lg: 1.25rem !default; + +// Intentionally left for folks who want it +// $btn-padding-y-xl: .75rem !default; +// $btn-padding-x-xl: 1.25rem !default; +// $btn-font-size-xl: var(--#{$prefix}font-size-lg) !default; +// $btn-line-height-xl: 1.5rem !default; $btn-border-width: $input-btn-border-width !default; $btn-font-weight: $font-weight-normal !default; $btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; -// $btn-focus-width: $input-btn-focus-width !default; -// $btn-focus-box-shadow: $input-btn-focus-box-shadow !default; $btn-disabled-opacity: .65 !default; $btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default; $btn-link-color: var(--#{$prefix}link-color) !default; $btn-link-hover-color: var(--#{$prefix}link-hover-color) !default; -$btn-link-disabled-color: var(--#{$prefix}gray-600) !default; -// $btn-link-color-contrast: color-contrast($link-color) !default; -// $btn-link-focus-shadow-rgb: to-rgb(color.mix($btn-link-color-contrast, $link-color, 15%)) !default; +$btn-link-disabled-color: var(--#{$prefix}fg-3) !default; // Allows for customizing button radius independently from global border radius $btn-border-radius: var(--#{$prefix}border-radius) !default; +$btn-border-radius-xs: var(--#{$prefix}border-radius-sm) !default; $btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; $btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; +$btn-border-radius-xl: var(--#{$prefix}border-radius-lg) !default; $btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$btn-hover-bg-shade-amount: 15% !default; -$btn-hover-bg-tint-amount: 15% !default; -$btn-hover-border-shade-amount: 20% !default; -$btn-hover-border-tint-amount: 10% !default; -$btn-active-bg-shade-amount: 20% !default; -$btn-active-bg-tint-amount: 20% !default; -$btn-active-border-shade-amount: 25% !default; -$btn-active-border-tint-amount: 10% !default; // scss-docs-end btn-variables diff --git a/scss/buttons/_button.scss b/scss/buttons/_button.scss index 100b811996..2515dda2b7 100644 --- a/scss/buttons/_button.scss +++ b/scss/buttons/_button.scss @@ -2,6 +2,7 @@ @use "sass:list"; @use "sass:map"; @use "sass:meta"; +@use "sass:string"; @use "../colors" as *; @use "../config" as *; @use "../theme" as *; @@ -84,73 +85,58 @@ $button-variants: ( ) !default; // scss-docs-end btn-variants -// // Main button style generator mixin - -// Generate button variant classes (e.g., .btn-solid, .btn-outline, etc.) -// scss-docs-start btn-variant-mixin -@each $variant, $_ in $button-variants { - .btn-#{$variant} { - @each $property, $value in map.get($button-variants, $variant, "base") { - @if $value == "transparent" { - --#{$prefix}btn-#{$property}: transparent; - } @else { - --#{$prefix}btn-#{$property}: var(--#{$prefix}theme-#{$value}); - } - } - - &:hover { - @each $property, $value in map.get($button-variants, $variant, "hover") { - @if $value == "transparent" { - --#{$prefix}btn-hover-#{$property}: transparent; - } @else if meta.type-of($value) == "list" { - $first-value: list.nth($value, 1); - $second-value: list.nth($value, 2); - --#{$prefix}btn-hover-#{$property}: color-mix(in oklch, var(--#{$prefix}theme-#{$first-value}) 50%, var(--#{$prefix}theme-#{$second-value})); - } @else if $value == "bg-subtle" { - --#{$prefix}btn-hover-#{$property}: var(--#{$prefix}theme-#{$value}); - } @else { - --#{$prefix}btn-hover-#{$property}: oklch(from var(--#{$prefix}theme-#{$value}) calc(l * .95) calc(c * 1.1) h); - } - } - } - - &:focus-visible { - outline-color: var(--#{$prefix}theme-focus-ring); - } - - &:active, - &.active { - @each $property, $value in map.get($button-variants, $variant, "active") { - @if $value == "transparent" { - --#{$prefix}btn-active-#{$property}: transparent; - } @else if $value == "bg-subtle" { - --#{$prefix}btn-active-#{$property}: var(--#{$prefix}theme-#{$value}); - } @else { - --#{$prefix}btn-active-#{$property}: oklch(from var(--#{$prefix}theme-#{$value}) calc(l * .9) calc(c * 1.15) h); - } - } - } - } -} -// scss-docs-end btn-variant-mixin - -// scss-docs-start btn-size-mixin -@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) { - --#{$prefix}btn-padding-y: #{$padding-y}; - --#{$prefix}btn-padding-x: #{$padding-x}; - @include rfs($font-size, --#{$prefix}btn-font-size); - --#{$prefix}btn-border-radius: #{$border-radius}; -} -// scss-docs-end btn-size-mixin +// scss-docs-start btn-sizes +$button-sizes: ( + "xs": ( + "padding-y": $btn-padding-y-xs, + "padding-x": $btn-padding-x-xs, + "font-size": $btn-font-size-xs, + "line-height": $btn-line-height-xs, + "border-radius": $btn-border-radius-xs, + "min-height": 1.5rem + ), + "sm": ( + "padding-y": $btn-padding-y-sm, + "padding-x": $btn-padding-x-sm, + "font-size": $btn-font-size-sm, + "line-height": $btn-line-height-sm, + "border-radius": $btn-border-radius-sm, + "min-height": 2rem + ), + "lg": ( + "padding-y": $btn-padding-y-lg, + "padding-x": $btn-padding-x-lg, + "font-size": $btn-font-size-lg, + "line-height": $btn-line-height-lg, + "border-radius": $btn-border-radius-lg, + "min-height": 2.75rem + ), + // Commented out by default on purpose + // "xl": ( + // "padding-y": $btn-padding-y-xl, + // "padding-x": $btn-padding-x-xl, + // "font-size": $btn-font-size-xl, + // "line-height": $btn-line-height-xl, + // "border-radius": $btn-border-radius-xl, + // "min-height": 3.25rem + // ), +) !default; +// scss-docs-end btn-sizes // // Base styles // +$btn-variant-selectors: () !default; +@each $variant in map.keys($button-variants) { + $btn-variant-selectors: list.append($btn-variant-selectors, string.unquote(".btn-#{$variant}"), comma); +} + @layer components { .btn, - [class*="btn-"] { + #{$btn-variant-selectors} { // scss-docs-start btn-css-vars + --#{$prefix}btn-min-height: 2.25rem; --#{$prefix}btn-padding-x: #{$btn-padding-x}; --#{$prefix}btn-padding-y: #{$btn-padding-y}; --#{$prefix}btn-font-family: #{$btn-font-family}; @@ -158,91 +144,150 @@ $button-variants: ( --#{$prefix}btn-font-weight: #{$btn-font-weight}; --#{$prefix}btn-line-height: #{$btn-line-height}; --#{$prefix}btn-color: #{$btn-color}; - --#{$prefix}btn-bg: transparent; --#{$prefix}btn-border-width: #{$btn-border-width}; --#{$prefix}btn-border-color: transparent; --#{$prefix}btn-border-radius: #{$btn-border-radius}; --#{$prefix}btn-hover-border-color: transparent; - --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; - // --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); // scss-docs-end btn-css-vars - display: inline-block; + display: inline-flex; + gap: var(--#{$prefix}btn-gap, .25rem); + align-items: center; + justify-content: center; + min-height: var(--#{$prefix}btn-min-height); padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); font-family: var(--#{$prefix}btn-font-family); @include font-size(var(--#{$prefix}btn-font-size)); font-weight: var(--#{$prefix}btn-font-weight); line-height: var(--#{$prefix}btn-line-height); color: var(--#{$prefix}btn-color); - text-align: center; text-decoration: none; white-space: $btn-white-space; vertical-align: middle; cursor: if($enable-button-pointers, pointer, null); user-select: none; + background-color: var(--#{$prefix}btn-bg, var(--#{$prefix}bg-2)); border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); @include border-radius(var(--#{$prefix}btn-border-radius)); - @include gradient-bg(var(--#{$prefix}btn-bg)); - @include box-shadow(var(--#{$prefix}btn-box-shadow)); @include transition($btn-transition); &:hover { color: var(--#{$prefix}btn-hover-color); - background-color: var(--#{$prefix}btn-hover-bg); + background-color: var(--#{$prefix}btn-hover-bg, var(--#{$prefix}bg-3)); border-color: var(--#{$prefix}btn-hover-border-color); } - .btn-check + &:hover { - // override for the checkbox/radio buttons - color: var(--#{$prefix}btn-color); - background-color: var(--#{$prefix}btn-bg); - border-color: var(--#{$prefix}btn-border-color); - } - &:focus-visible { @include focus-ring(true); --#{$prefix}focus-ring-offset: 1px; } - .btn-check:focus-visible + & { - @include focus-ring(true); - } - - .btn-check:checked + &, - :not(.btn-check) + &:active, - &:first-child:active, &.active, &.show { color: var(--#{$prefix}btn-active-color); - background-color: var(--#{$prefix}btn-active-bg); - // Remove CSS gradients if they're enabled - background-image: if($enable-gradients, none, null); + background-color: var(--#{$prefix}btn-active-bg, var(--#{$prefix}bg-3)); border-color: var(--#{$prefix}btn-active-border-color); - @include box-shadow(var(--#{$prefix}btn-active-shadow)); &:focus-visible { @include focus-ring(true); } } - .btn-check:checked:focus-visible + & { - @include focus-ring(true); - } - &:disabled, &.disabled, fieldset:disabled & { color: var(--#{$prefix}btn-disabled-color); pointer-events: none; - background-color: var(--#{$prefix}btn-disabled-bg); + background-color: var(--#{$prefix}btn-disabled-bg, var(--#{$prefix}bg-1)); background-image: if($enable-gradients, none, null); border-color: var(--#{$prefix}btn-disabled-border-color); opacity: var(--#{$prefix}btn-disabled-opacity); - @include box-shadow(none); } } + + // Main button style generator mixin + // Generate button variant classes (e.g., .btn-solid, .btn-outline, etc.) + // scss-docs-start btn-variant-mixin + @each $variant, $_ in $button-variants { + .btn-#{$variant} { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --#{$prefix}btn-#{$property}: transparent; + } @else { + --#{$prefix}btn-#{$property}: var(--#{$prefix}theme-#{$value}); + } + } + + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --#{$prefix}btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --#{$prefix}btn-active-#{$property}: var(--#{$prefix}theme-#{$value}); + } @else { + --#{$prefix}btn-active-#{$property}: oklch(from var(--#{$prefix}theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --#{$prefix}btn-disabled-#{$property}: transparent; + } @else { + --#{$prefix}btn-disabled-#{$property}: var(--#{$prefix}theme-#{$value}); + } + } + + + &:hover { + @each $property, $value in map.get($button-variants, $variant, "hover") { + @if $value == "transparent" { + --#{$prefix}btn-hover-#{$property}: transparent; + } @else if meta.type-of($value) == "list" { + $first-value: list.nth($value, 1); + $second-value: list.nth($value, 2); + --#{$prefix}btn-hover-#{$property}: color-mix(in oklch, var(--#{$prefix}theme-#{$first-value}) 50%, var(--#{$prefix}theme-#{$second-value})); + } @else if $value == "bg-subtle" { + --#{$prefix}btn-hover-#{$property}: var(--#{$prefix}theme-#{$value}); + } @else { + --#{$prefix}btn-hover-#{$property}: oklch(from var(--#{$prefix}theme-#{$value}) calc(l * .95) calc(c * 1.1) h); + } + } + } + + &:focus-visible { + outline-color: var(--#{$prefix}theme-focus-ring); + } + + &:active, + &.active, + &.btn-check:has(input:checked) { + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --#{$prefix}btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --#{$prefix}btn-active-#{$property}: var(--#{$prefix}theme-#{$value}); + } @else { + --#{$prefix}btn-active-#{$property}: oklch(from var(--#{$prefix}theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + } + + // Disabled state for toggle buttons + &:disabled, + &.disabled, + &.btn-check:has(input:disabled) { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --#{$prefix}btn-disabled-#{$property}: transparent; + } @else { + --#{$prefix}btn-disabled-#{$property}: var(--#{$prefix}theme-#{$value}); + } + } + } + } + } + // scss-docs-end btn-variant-mixin + // // Link buttons // @@ -284,26 +329,110 @@ $button-variants: ( // Button Sizes // - .btn-lg { - @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); + // Generate button size classes from the $button-sizes map + // Skip "md" as it's the default size for .btn + @each $size, $properties in $button-sizes { + .btn-#{$size} { + --#{$prefix}btn-min-height: #{map.get($properties, "min-height")}; + --#{$prefix}btn-padding-y: #{map.get($properties, "padding-y")}; + --#{$prefix}btn-padding-x: #{map.get($properties, "padding-x")}; + --#{$prefix}btn-font-size: #{map.get($properties, "font-size")}; + --#{$prefix}btn-line-height: #{map.get($properties, "line-height")}; + --#{$prefix}btn-border-radius: #{map.get($properties, "border-radius")}; + } } - .btn-sm { - @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); + .btn-icon { + align-items: center; + justify-content: center; + aspect-ratio: 1; + padding: 0; } + // + // Toggle buttons (.btn-check) + // + // Checkbox and radio inputs that look like buttons. Add .btn-check to a + // label with button classes, with the input nested inside. + // + // Example: + .btn-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; - - &[disabled], - &:disabled { - + .btn { - pointer-events: none; - filter: none; - opacity: .65; - } + > input { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; + } + + &:has(input:checked) { + color: var(--#{$prefix}btn-active-color); + background-color: var(--#{$prefix}btn-active-bg, var(--#{$prefix}bg-3)); + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-active-border-color); + @include box-shadow(var(--#{$prefix}btn-active-shadow)); + } + + &:has(input:focus-visible) { + @include focus-ring(true); + --#{$prefix}focus-ring-offset: 1px; + } + + &:has(input:disabled) { + color: var(--#{$prefix}btn-disabled-color); + pointer-events: none; + background-color: var(--#{$prefix}btn-disabled-bg, var(--#{$prefix}bg-1)); + background-image: if($enable-gradients, none, null); + border-color: var(--#{$prefix}btn-disabled-border-color); + opacity: var(--#{$prefix}btn-disabled-opacity); + @include box-shadow(none); + } + } + + // + // Styled buttons + // + // Add visual depth with gradients and shadows. Customize via CSS variables. + + .btn-styled { + --#{$prefix}btn-gradient-start: rgb(255 255 255 / 12.5%); + --#{$prefix}btn-gradient-end: rgb(0 0 0 / 7.5%); + --#{$prefix}btn-border-mix-color: #000; + --#{$prefix}btn-border-mix-amount: 10%; + --#{$prefix}btn-border-hover-mix-amount: 12.5%; + --#{$prefix}btn-border-active-mix-amount: 20%; + --#{$prefix}btn-shadow: 0 1px 2px rgb(0 0 0 / 15%), inset 0 1px 0 rgb(255 255 255 / 10%); + --#{$prefix}btn-active-shadow: inset 0 2px 4px rgba(0, 0, 0, .15); + + background-image: + linear-gradient( + to bottom, + var(--#{$prefix}btn-gradient-start), + var(--#{$prefix}btn-gradient-end) + ); + border-color: color-mix(in lab, var(--#{$prefix}theme-bg), var(--#{$prefix}btn-border-mix-color) var(--#{$prefix}btn-border-mix-amount)); + box-shadow: var(--#{$prefix}btn-shadow); + + &:hover { + background-image: + linear-gradient( + to bottom, + var(--#{$prefix}btn-gradient-start), + var(--#{$prefix}btn-gradient-end) + ); + border-color: color-mix(in lab, var(--#{$prefix}theme-bg), var(--#{$prefix}btn-border-mix-color) var(--#{$prefix}btn-border-hover-mix-amount)); + } + + &:active, + &.active { + background-image: none; + border-color: color-mix(in lab, var(--#{$prefix}theme-bg), var(--#{$prefix}btn-border-mix-color) var(--#{$prefix}btn-border-active-mix-amount)); + box-shadow: var(--#{$prefix}btn-active-shadow); + } + + &:disabled, + &.disabled { + background-image: none; + box-shadow: none; } } } diff --git a/site/src/components/shortcodes/ButtonPlayground.astro b/site/src/components/shortcodes/ButtonPlayground.astro new file mode 100644 index 0000000000..3bb36d9884 --- /dev/null +++ b/site/src/components/shortcodes/ButtonPlayground.astro @@ -0,0 +1,329 @@ +--- +import { getData } from '@libs/data' +import Example from '@components/shortcodes/Example.astro' + +const themeColors = getData('theme-colors') +const styles = ['solid', 'styled', 'outline', 'subtle', 'text'] +const sizes = [ + { value: 'xs', label: 'Extra small' }, + { value: 'sm', label: 'Small' }, + { value: '', label: 'Medium' }, + { value: 'lg', label: 'Large' } +] +const rounded = ['default', 'pill', 'square'] +--- + + + + + + + + + + + + + +
+
+
+ + +
+ +
+ +
+ {styles.map((style) => ( + + ))} +
+
+ +
+ + +
+ +
+ +
+ {rounded.map((round) => ( + + ))} +
+
+
+
+ +Button + + +`} + id="button-preview" +/> + + diff --git a/site/src/content/docs/components/button-group.mdx b/site/src/content/docs/components/button-group.mdx index 34a3055ad5..635b186b09 100644 --- a/site/src/content/docs/components/button-group.mdx +++ b/site/src/content/docs/components/button-group.mdx @@ -47,25 +47,33 @@ These classes can also be added to groups of links, as an alternative to the [`. Combine button-like checkbox and radio toggle buttons into a seamless looking button group. - - - - - - - - + + + `} /> - - - - - - - - + + + `} /> ## Button toolbar @@ -211,10 +219,16 @@ Make a set of buttons appear vertically stacked rather than horizontally. **Spli `} /> - - - - - - + + + `} /> diff --git a/site/src/content/docs/components/buttons.mdx b/site/src/content/docs/components/buttons.mdx index 0d161ccf4b..3819ea1eaf 100644 --- a/site/src/content/docs/components/buttons.mdx +++ b/site/src/content/docs/components/buttons.mdx @@ -8,19 +8,25 @@ import { getData } from '@libs/data' ## Base class -Bootstrap has a base `.btn` class that sets up basic styles such as padding and content alignment. By default, `.btn` controls have a transparent border and background color, and lack any explicit focus and hover styles. +Buttons have a standard `.btn` class that sets up basic styles such as padding and content alignment, along with fallback visual styles for basic accessibility. Change this class to another button class to apply different visual styles for a specific button variant and theme. Base class`} /> -The `.btn` class is intended to be used in conjunction with our button variants, or to serve as a basis for your own custom styles. +The `.btn` class is intended to be a starting point for your own custom button styles, while our provided button variants used in conjunction with our button variants, or to serve as a basis for your own custom styles. When using `.btn` without a modifier, be sure to add some explicit `:focus-visible` styles. +## Playground + +Bootstrap includes several button variants, each serving its own semantic purpose, with a few extras thrown in for more control. Combined with our utilities, you have a very powerful set of defaults to choose from. + + + ## Variants -Bootstrap includes several button variants, each serving its own semantic purpose, with a few extras thrown in for more control. +Compare all variants at once: ` @@ -31,6 +37,27 @@ Bootstrap includes several button variants, each serving its own semantic purpos
+## Styled buttons + +Add visual depth to solid buttons with gradients and shadows using the `.btn-styled` modifier class. This provides a more three-dimensional appearance that can be customized further with CSS variables. + + ``)]} /> + +The gradient and shadow can be customized via CSS variables: + + +| Variable | Description | +| --- | --- | +| `--bs-btn-gradient-start` | Top of gradient (default: `rgb(255 255 255 / 12.5%)`) | +| `--bs-btn-gradient-end` | Bottom of gradient (default: `rgb(0 0 0 / 7.5%)`) | +| `--bs-btn-border-mix-color` | Color to mix with the button background (default: `#000`) | +| `--bs-btn-border-mix-amount` | Amount of color to mix with the button background (default: `10%`) | +| `--bs-btn-border-hover-mix-amount` | Amount of color to mix with the button background on hover (default: `12.5%`) | +| `--bs-btn-border-active-mix-amount` | Amount of color to mix with the button background on active (default: `20%`) | +| `--bs-btn-shadow` | Resting state shadow | +| `--bs-btn-active-shadow` | Pressed/active state shadow | + + ## Disable text wrapping If you don’t want the button text to wrap, you can add the `.text-nowrap` class to the button. In Sass, you can set `$btn-white-space: nowrap` to disable text wrapping for each button. @@ -41,198 +68,184 @@ The `.btn` classes are designed to be used with the ` - - -`} /> - -## Outline buttons - -In need of a button, but not the hefty background colors they bring? Replace the default modifier classes with the `.btn-outline-*` ones to remove all background images and colors on any button. - - ``)} /> - - -Some of the button styles use a relatively light foreground color, and should only be used on a dark background in order to have sufficient contrast. - +Link + + + +`} /> ## Sizes Fancy larger or smaller buttons? Add `.btn-lg` or `.btn-sm` for additional sizes. -Large button -`} /> +Large button +`} /> + +Small button +`} /> -Small button -`} /> +Extra small button +`} /> You can even roll your own custom sizing with CSS variables: - Custom button `} /> +## Icon buttons + +Use `.btn-icon` for square buttons that contain only an icon. This class sets `aspect-ratio: 1` and removes padding, making the button perfectly square based on its `min-height`. Always include an `aria-label` attribute on icon-only buttons to ensure they are accessible to screen reader users. + + + + + + `} /> + +Combine with size classes for different icon button sizes: + + + + + + + `} /> + ## Disabled state -Make buttons look inactive by adding the `disabled` boolean attribute to any ` - - -`} /> +Primary button + + +`} /> -Disabled buttons using the `` element behave a bit different: +### Disabled links -- ``s don’t support the `disabled` attribute, so you must add the `.disabled` class to make it visually appear disabled. -- Some future-friendly styles are included to disable all `pointer-events` on anchor buttons. -- Disabled buttons using `` should include the `aria-disabled="true"` attribute to indicate the state of the element to assistive technologies. -- Disabled buttons using `` *should not* include the `href` attribute. +Disabled buttons using the `` element behave a bit different, and if you need to keep the `href` attribute, you may need additional HTML to fully disable the link functionality. Use the `.disabled` class instead of the attribute on `` elements to make them visually appear disabled. -Primary link -Link`} /> +Primary link +Link`} /> -### Link functionality caveat +If there's an `href` attribute, add `tabindex="-1"` and `aria-disabled="true"` to prevent keyboard focus and assistive technologies from interacting with the link. -To cover cases where you have to keep the `href` attribute on a disabled link, the `.disabled` class uses `pointer-events: none` to try to disable the link functionality of ``s. Note that this CSS property is not yet standardized for HTML, but all modern browsers support it. In addition, even in browsers that do support `pointer-events: none`, keyboard navigation remains unaffected, meaning that sighted keyboard users and users of assistive technologies will still be able to activate these links. So to be safe, in addition to `aria-disabled="true"`, also include a `tabindex="-1"` attribute on these links to prevent them from receiving keyboard focus, and use custom JavaScript to disable their functionality altogether. +Primary link +Link`} /> -Primary link -Link`} /> +You may also want to use custom JavaScript to disable the link functionality altogether. ## Block buttons -Create responsive stacks of full-width, “block buttons” like those in Bootstrap 4 with a mix of our display and gap utilities. By using utilities instead of button-specific classes, we have much greater control over spacing, alignment, and responsive behaviors. +Use the `.d-grid` utility to create responsive stacks of full-width “block buttons”. Space them out with gap utilities. - - + + `} /> Here we create a responsive variation, starting with vertically stacked buttons until the `md` breakpoint, where `.d-md-block` replaces the `.d-grid` class, thus nullifying the `gap-2` utility. Resize your browser to see them change. - - + + `} /> You can adjust the width of your block buttons with grid column width classes. For example, for a half-width “block button”, use `.col-6`. Center it horizontally with `.mx-auto`, too. - - + + `} /> Additional utilities can be used to adjust the alignment of buttons when horizontal. Here we’ve taken our previous responsive example and added some flex utilities and a margin utility on the button to right-align the buttons when they’re no longer stacked. - - + + `} /> ## Toggle buttons -Create button-like checkboxes and radio buttons by using `.btn` styles rather than `.form-check-label` on the ` -Different variants of `.btn`, such as the various outlined styles, are supported. + - -
+ - -
- - - - - -`} /> + `} /> ## Button plugin The button plugin allows you to create simple on/off toggle buttons. -Visually, these toggle buttons are identical to the [checkbox toggle buttons]([[docsref:/forms/checkbox]]). However, they are conveyed differently by assistive technologies: the checkbox toggles will be announced by screen readers as “checked”/“not checked” (since, despite their appearance, they are fundamentally still checkboxes), whereas these toggle buttons will be announced as “button”/“button pressed”. The choice between these two approaches will depend on the type of toggle you are creating, and whether or not the toggle will make sense to users when announced as a checkbox or as an actual button. +Visually, these toggle buttons are identical to our [input toggle buttons](#toggle-buttons), but are conveyed differently by assistive technologies. Checkbox toggles will be announced by screen readers as "checked"/"not checked"as they are checkboxes under the hood. The JavaScript toggle buttons are announced as "button"/"button pressed". It’s up to you to decide which approach makes more sense for your use case. ### Toggle states Add `data-bs-toggle="button"` to toggle a button’s `active` state. If you’re pre-toggling a button, you must manually add the `.active` class **and** `aria-pressed="true"` to ensure that it is conveyed appropriately to assistive technologies. - - - - -

-

- - - -

`} /> - - - Toggle link - Active toggle link - Disabled toggle link -

-

- Toggle link - Active toggle link - Disabled toggle link -

`} /> +Toggle button + + `} /> + +Toggle link + Active toggle link + Disabled toggle link`} /> ### Methods @@ -264,44 +277,111 @@ document.querySelectorAll('.btn').forEach(buttonElement => { ### Variables - +Buttons use CSS variables for real-time customization. These are set on the base `.btn` class and inherited by all button variants. -Each `.btn-*` modifier class updates the appropriate CSS variables to minimize additional CSS rules with our `button-variant()`, `button-outline-variant()`, and `button-size()` mixins. - -Here’s an example of building a custom `.btn-*` modifier class as we do for the buttons unique to our docs by reassigning Bootstrap’s CSS variables with a mixture of our own CSS and Sass variables. - -Custom button -`} /> - - +Each variant class (`.btn-solid`, `.btn-outline`, etc.) updates these variables based on the current theme color, allowing buttons to adapt automatically when used with `.theme-*` classes. ### Sass variables +Base button styling uses these Sass variables for padding, typography, and sizing across all button variants. + -### Sass map +### Sass maps -Button variants—including all their states—are defined in the `$button-variants` Sass map. This map identifies which theme color tokens to use for each variant's state. +#### Variants map -For example, a solid button uses the same `bg` token for its background and border colors because we want it to have a seamless look. +Button variants are defined in the `$button-variants` Sass map. Each variant specifies which theme color tokens to use for its base, hover, and active states. The map uses token names (like `"bg"`, `"contrast"`, `"border"`) that reference the current theme's color palette. -### Sass mixins +To add a custom variant, extend the map before importing Bootstrap’s buttons: + +```scss +@use "bootstrap/scss/buttons/button" with ( + $button-variants: ( + "solid": ( /* ... */ ), + "outline": ( /* ... */ ), + "subtle": ( /* ... */ ), + "text": ( /* ... */ ), + "ghost": ( + "base": ( + "bg": "transparent", + "color": "text", + "border-color": "transparent" + ), + "hover": ( + "bg": "bg-subtle", + "color": "text-emphasis" + ), + "active": ( + "bg": "bg-subtle", + "color": "text-emphasis" + ) + ) + ) +); +``` -There are three mixins for buttons: button and button outline variant mixins (both based on `$theme-colors`), plus a button size mixin. +To remove a variant you don't need, use Sass's `map.remove()`: - +```scss +@use "sass:map"; -{/* +@use "bootstrap/scss/buttons/button" as *button* with ( + $button-variants: map.remove($button-variants, "text", "subtle") +); +``` -*/} +#### Sizes map + +Button sizes are defined in the `$button-sizes` map. Each size specifies padding, font size, line height, border radius, and minimum height. The loop generates `.btn-xs`, `.btn-sm`, and `.btn-lg` classes. + + + +To add or modify sizes, extend the map: + +```scss +@use "bootstrap/scss/buttons/button" with ( + $button-sizes: ( + "xs": ( /* ... */ ), + "sm": ( /* ... */ ), + "lg": ( /* ... */ ), + "xl": ( + "padding-y": .75rem, + "padding-x": 1.25rem, + "font-size": var(--bs-font-size-lg), + "line-height": 1.5rem, + "border-radius": var(--bs-border-radius-lg), + "min-height": 3.25rem + ) + ) +); +``` ### Sass loops -Button variants (for regular and outline buttons) use their respective mixins with our `$theme-colors` map to generate the modifier classes in `scss/_buttons.scss`. +#### Variant loop + +The variant loop iterates over `$button-variants` to generate each variant class. It sets CSS variables for base, hover, active, and disabled states using the theme color tokens defined in the map. -{/* */} + + +#### Size loop + +The size loop iterates over `$button-sizes` to generate size modifier classes, setting the appropriate CSS variables for each size. + +```scss +@each $size, $properties in $button-sizes { + .btn-#{$size} { + --bs-btn-min-height: #{map.get($properties, "min-height")}; + --bs-btn-padding-y: #{map.get($properties, "padding-y")}; + --bs-btn-padding-x: #{map.get($properties, "padding-x")}; + --bs-btn-font-size: #{map.get($properties, "font-size")}; + --bs-btn-line-height: #{map.get($properties, "line-height")}; + --bs-btn-border-radius: #{map.get($properties, "border-radius")}; + } +} +``` diff --git a/site/src/content/docs/getting-started/approach.mdx b/site/src/content/docs/getting-started/approach.mdx index b7161bbec1..9ceea05f34 100644 --- a/site/src/content/docs/getting-started/approach.mdx +++ b/site/src/content/docs/getting-started/approach.mdx @@ -71,7 +71,7 @@ We use [Autoprefixer](https://github.com/postcss/autoprefixer) to handle intende ## Guiding principles -Beyond what Bootstrap does, here's _why_ we do it—our philosophy for building on the web. At a high level, here's what guides our approach: +Beyond what Bootstrap does, here's *why* we do it—our philosophy for building on the web. At a high level, here's what guides our approach: - Components should be responsive and mobile-first - Components should be built with a base class and extended via modifier classes @@ -101,7 +101,7 @@ We use two `z-index` scales in Bootstrap—elements within a component and overl Some components in Bootstrap are built with overlapping elements to prevent double borders without modifying the `border` property. For example, button groups, input groups, and pagination. These components share a standard `z-index` scale of `0` through `3`, matching our expectations of highest user priority. - `0` is for default states (initial, not actually set) -- `1` is for `:hover`, lowest because while it indicates user intent, nearly _anything_ can be hovered. +- `1` is for `:hover`, lowest because while it indicates user intent, nearly *anything* can be hovered. - `2` is for `:active`/`.active`, second highest because they indicate state. - `3` is for `:focus`, highest because focused elements are in view and at the user’s attention. diff --git a/site/src/content/docs/guides/quickstart.mdx b/site/src/content/docs/guides/quickstart.mdx index a5c06346d7..b44db56e9d 100644 --- a/site/src/content/docs/guides/quickstart.mdx +++ b/site/src/content/docs/guides/quickstart.mdx @@ -27,7 +27,7 @@ Get started using Bootstrap in seconds by including our production-ready CSS and ``` -2. **Include Bootstrap’s CSS and JS.** Place the `` tag in the `` for our CSS, and the `