With this refactor, our .fg-* and .bg-* and .border-* utilities can be modified by custom CSS, like in our blue theme example.
This also standardizes some nomenclature by using fg instead of text in the token names for theme values, better matching bg. Classes already use this, as do root fg tokens, so this is just a consistency update.
border-color: var(--theme-border, var(--accordion-border-color));
> .accordion-header {
- color: var(--theme-text, var(--accordion-active-color));
+ color: var(--theme-fg, var(--accordion-active-color));
background-color: var(--theme-bg-subtle, var(--accordion-active-bg));
box-shadow: inset 0 calc(-1 * var(--accordion-border-width)) 0 var(--theme-border, var(--accordion-border-color));
--alert-bg: var(--theme-bg-subtle, var(--bg-1)),
--alert-padding-x: #{$spacer},
--alert-padding-y: #{$spacer},
- --alert-color: var(--theme-text, inherit),
+ --alert-color: var(--theme-fg, inherit),
--alert-border-color: var(--theme-border, var(--border-color)),
--alert-border: var(--border-width) solid var(--alert-border-color),
--alert-border-radius: var(--border-radius),
}
.avatar-subtle {
- color: var(--theme-text, var(--avatar-color));
+ color: var(--theme-fg, var(--avatar-color));
background-color: var(--theme-bg-subtle, var(--avatar-bg));
}
border-color: var(--theme-border, var(--card-border-color));
.card-header {
- color: var(--theme-text-emphasis, currentcolor);
+ color: var(--theme-fg-emphasis, currentcolor);
background-color: var(--theme-bg-subtle, var(--card-cap-bg));
border-color: var(--theme-border, var(--card-border-color));
}
.card-footer {
- color: var(--theme-text-emphasis, currentcolor);
+ color: var(--theme-fg-emphasis, currentcolor);
background-color: var(--theme-bg-subtle, var(--card-cap-bg));
border-color: var(--theme-border, var(--card-border-color));
}
--chip-dismiss-size: 1rem,
--chip-dismiss-opacity: .65,
--chip-dismiss-hover-opacity: 1,
- --chip-color: var(--theme-text, var(--fg-body)),
+ --chip-color: var(--theme-fg, var(--fg-body)),
--chip-bg: var(--theme-bg-subtle, var(--bg-2)),
--chip-border-color: transparent,
--chip-selected-color: var(--theme-contrast, var(--primary-contrast)),
position: relative;
display: block;
padding: var(--list-group-item-padding-y) var(--list-group-item-padding-x);
- color: var(--theme-text, var(--list-group-color));
+ color: var(--theme-fg, var(--list-group-color));
// stylelint-disable-next-line scss/at-function-named-arguments
text-decoration: if(sass($link-decoration == none): null);
background-color: var(--theme-bg-subtle, var(--list-group-bg));
.list-group-item-action {
width: 100%; // For `<button>`s (anchors become 100% by default though)
- color: var(--theme-text, var(--list-group-action-color));
+ color: var(--theme-fg, var(--list-group-action-color));
text-align: inherit; // For `<button>`s (anchors inherit)
text-decoration: none;
&:hover,
&:focus {
z-index: 1; // Place hover/focus items above their siblings for proper border styling
- color: var(--theme-text-emphasis, var(--list-group-action-hover-color));
+ color: var(--theme-fg-emphasis, var(--list-group-action-hover-color));
text-decoration: none;
background-color: var(--theme-bg-muted, var(--list-group-action-hover-bg));
}
&:active {
- color: var(--theme-text-emphasis, var(--list-group-action-active-color));
+ color: var(--theme-fg-emphasis, var(--list-group-action-active-color));
background-color: var(--theme-bg-muted, var(--list-group-action-active-bg));
}
}
width: 100%;
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
font-weight: var(--menu-item-font-weight, var(--font-weight-normal));
- color: var(--theme-text, var(--menu-item-color));
+ color: var(--theme-fg, var(--menu-item-color));
text-align: inherit;
text-decoration: none;
white-space: nowrap;
&:hover,
&:focus {
- color: var(--theme-text-emphasis, var(--menu-item-hover-color));
+ color: var(--theme-fg-emphasis, var(--menu-item-hover-color));
background-color: var(--theme-bg-subtle, var(--menu-item-hover-bg));
// @include gradient-bg(var(--theme-bg-subtle, var(--menu-item-hover-bg)));
}
--hr-border-color: var(--border-color),
- --link-color: light-dark(var(--primary-base), var(--primary-text)),
+ --link-color: light-dark(var(--primary-base), var(--primary-fg)),
--link-decoration: #{$link-decoration},
--link-hover-color: color-mix(in oklch, var(--link-color) 90%, #000),
@return $result;
}
+// Themes map sub-keys
+//
+// Return var() references to root tokens instead of raw values.
+// Ex: theme-color-refs("bg") => (primary: var(--primary-bg), accent: var(--accent-bg), ...)
+@function theme-color-refs($key) {
+ $result: ();
+
+ @each $color-name, $color-map in $theme-colors {
+ @if map.has-key($color-map, $key) {
+ $result: map.merge($result, ($color-name: var(--#{$color-name}-#{$key})));
+ }
+ }
+
+ @return $result;
+}
+
+// Theme token to root tokens
+//
+// Returns the global :root token reference for a given a given token map, prefix, and key.
+// Ex: theme-token-refs($theme-bgs, "bg") => (body: var(--bg-body), 1: var(--bg-1), ...)
+// Skips `inherit` since it's a CSS-wide keyword that can't be stored in a custom property.
+@function theme-token-refs($map, $prefix) {
+ $result: ();
+
+ @each $key, $value in $map {
+ @if $value != inherit {
+ $result: map.merge($result, ($key: var(--#{$prefix}-#{$key})));
+ }
+ }
+
+ @return $result;
+}
+
// Generate opacity values using color-mix()
@function theme-opacity-values($color-var, $opacities: $util-opacity) {
$result: ();
$theme-colors: (
"primary": (
"base": var(--blue-500),
- "text": light-dark(var(--blue-600), var(--blue-400)),
- "text-emphasis": light-dark(var(--blue-800), var(--blue-200)),
+ "fg": light-dark(var(--blue-600), var(--blue-400)),
+ "fg-emphasis": light-dark(var(--blue-800), var(--blue-200)),
"bg": var(--blue-500),
"bg-subtle": light-dark(var(--blue-100), var(--blue-900)),
"bg-muted": light-dark(var(--blue-200), var(--blue-800)),
),
"accent": (
"base": var(--indigo-500),
- "text": light-dark(var(--indigo-600), color-mix(in oklch, var(--indigo-400), var(--indigo-300))),
- "text-emphasis": light-dark(var(--indigo-800), var(--indigo-300)),
+ "fg": light-dark(var(--indigo-600), color-mix(in oklch, var(--indigo-400), var(--indigo-300))),
+ "fg-emphasis": light-dark(var(--indigo-800), var(--indigo-300)),
"bg": var(--indigo-500),
"bg-subtle": light-dark(var(--indigo-100), var(--indigo-900)),
"bg-muted": light-dark(var(--indigo-200), var(--indigo-800)),
),
"success": (
"base": var(--green-500),
- "text": light-dark(var(--green-600), var(--green-400)),
- "text-emphasis": light-dark(var(--green-800), var(--green-300)),
+ "fg": light-dark(var(--green-600), var(--green-400)),
+ "fg-emphasis": light-dark(var(--green-800), var(--green-300)),
"bg": var(--green-500),
"bg-subtle": light-dark(var(--green-100), var(--green-900)),
"bg-muted": light-dark(var(--green-200), var(--green-800)),
),
"danger": (
"base": var(--red-500),
- "text": light-dark(var(--red-600), var(--red-400)),
- "text-emphasis": light-dark(var(--red-800), var(--red-300)),
+ "fg": light-dark(var(--red-600), var(--red-400)),
+ "fg-emphasis": light-dark(var(--red-800), var(--red-300)),
"bg": var(--red-500),
"bg-subtle": light-dark(var(--red-100), var(--red-900)),
"bg-muted": light-dark(var(--red-200), var(--red-800)),
),
"warning": (
"base": var(--yellow-500),
- "text": light-dark(var(--yellow-700), var(--yellow-400)),
- "text-emphasis": light-dark(var(--yellow-800), var(--yellow-300)),
+ "fg": light-dark(var(--yellow-700), var(--yellow-400)),
+ "fg-emphasis": light-dark(var(--yellow-800), var(--yellow-300)),
"bg": var(--yellow-500),
"bg-subtle": light-dark(var(--yellow-100), var(--yellow-900)),
"bg-muted": light-dark(var(--yellow-200), var(--yellow-800)),
),
"info": (
"base": var(--cyan-500),
- "text": light-dark(var(--cyan-600), var(--cyan-400)),
- "text-emphasis": light-dark(var(--cyan-800), var(--cyan-300)),
+ "fg": light-dark(var(--cyan-600), var(--cyan-400)),
+ "fg-emphasis": light-dark(var(--cyan-800), var(--cyan-300)),
"bg": var(--cyan-500),
"bg-subtle": light-dark(var(--cyan-100), var(--cyan-900)),
"bg-muted": light-dark(var(--cyan-200), var(--cyan-800)),
),
"inverse": (
"base": var(--gray-900),
- "text": light-dark(var(--gray-900), var(--gray-200)),
- "text-emphasis": light-dark(var(--gray-975), var(--white)),
+ "fg": light-dark(var(--gray-900), var(--gray-200)),
+ "fg-emphasis": light-dark(var(--gray-975), var(--white)),
"bg": light-dark(var(--gray-900), var(--gray-025)),
"bg-subtle": light-dark(var(--gray-100), var(--gray-900)),
"bg-muted": light-dark(var(--gray-200), var(--gray-300)),
),
"secondary": (
"base": var(--gray-200),
- "text": light-dark(var(--gray-600), var(--gray-400)),
- "text-emphasis": light-dark(var(--gray-800), var(--gray-200)),
+ "fg": light-dark(var(--gray-600), var(--gray-400)),
+ "fg-emphasis": light-dark(var(--gray-800), var(--gray-200)),
"bg": light-dark(var(--gray-100), var(--gray-600)),
"bg-subtle": light-dark(var(--gray-050), var(--gray-800)),
"bg-muted": light-dark(var(--gray-100), var(--gray-700)),
display: flex;
align-items: center;
padding: var(--toast-padding-y) var(--toast-padding-x);
- color: var(--theme-text-emphasis, var(--toast-header-color));
+ color: var(--theme-fg-emphasis, var(--toast-header-color));
background-color: var(--theme-bg-subtle, var(--toast-header-bg));
// background-clip: padding-box;
border-block-end: var(--toast-border-width, var(--border-width)) solid var(--theme-border, var(--toast-header-border-color, var(--border-color-translucent)));
"border-color": var(--border-color)
),
class: border,
- values: map.merge(theme-color-values("bg"), $theme-borders),
+ values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-borders, "border")),
),
"border-color-subtle": (
property: (
"border-color": var(--border-color)
),
class: border-subtle,
- values: theme-color-values("border"),
+ values: theme-color-refs("border"),
),
"border-width": (
property: border-width,
"color": var(--fg)
),
class: fg,
- values: map.merge(theme-color-values("text"), $theme-fgs),
+ values: map.merge(theme-color-refs("fg"), theme-token-refs($theme-fgs, "fg")),
),
"fg-emphasis": (
property: (
"color": var(--fg)
),
class: fg-emphasis,
- values: theme-color-values("text-emphasis"),
+ values: theme-color-refs("fg-emphasis"),
),
"fg-contrast": (
property: (
"color": var(--fg)
),
class: fg-contrast,
- values: theme-color-values("contrast"),
+ values: theme-color-refs("contrast"),
),
"fg-opacity": (
class: fg,
"underline-color": (
property: text-decoration-color,
class: underline,
- values: theme-color-values("text"),
+ values: theme-color-values("fg"),
),
"underline-opacity": (
property: text-decoration-color,
"background-color": var(--bg)
),
class: bg,
- values: map.merge(theme-color-values("bg"), $theme-bgs),
+ values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-bgs, "bg")),
),
"bg-color-subtle": (
property: (
"background-color": var(--bg)
),
class: bg-subtle,
- values: theme-color-values("bg-subtle"),
+ values: theme-color-refs("bg-subtle"),
),
"bg-color-muted": (
property: (
"background-color": var(--bg)
),
class: bg-muted,
- values: theme-color-values("bg-muted"),
+ values: theme-color-refs("bg-muted"),
),
"bg-opacity": (
class: bg,
"theme-subtle": (
property: (
"background-color": var(--theme-bg-subtle),
- "color": var(--theme-text)
+ "color": var(--theme-fg)
),
class: theme-subtle,
values: (null: null)
"theme-muted": (
property: (
"background-color": var(--theme-bg-muted),
- "color": var(--theme-text-emphasis)
+ "color": var(--theme-fg-emphasis)
),
class: theme-muted,
values: (null: null)
.btn-link {
@include tokens($button-link-tokens);
- color: var(--theme-text, var(--btn-color));
+ color: var(--theme-fg, var(--btn-color));
text-decoration: var(--link-decoration);
@if $enable-gradients {
}
&:focus-visible {
- color: var(--theme-text, var(--btn-color));
+ color: var(--theme-fg, var(--btn-color));
}
&:hover {
- color: var(--theme-text-emphasis, var(--btn-hover-color));
+ color: var(--theme-fg-emphasis, var(--btn-hover-color));
}
// No need for an active state here
$prose-tokens: defaults(
(
--content-font-size: 1rem,
- --content-gap: 20px,
+ --content-line-height: 1.5,
+ --content-gap: calc(var(--content-font-size) * var(--content-line-height)),
--heading-color: light-dark(var(--gray-900), var(--white)),
),
$prose-tokens
max-width: 1000px;
margin-inline: auto;
font-size: var(--content-font-size);
- line-height: 1.5;
+ line-height: var(--content-line-height);
@media (width >= 1024px) {
--content-font-size: var(--font-size-md);
- --content-gap: 24px;
+ --content-line-height: 1.625;
+ // --content-gap: calc(var(--content-font-size) * var(--content-line-height));
}
:where(p, ul, ol, dl, pre, table, blockquote):not(:where(.not-prose, .not-prose *)) {
// Links
a {
- color: var(--theme-text, var(--link-color));
+ color: var(--theme-fg, var(--link-color));
text-decoration: var(--link-decoration);
text-underline-offset: $link-underline-offset;
&:hover {
// --link-color: var(--link-hover-color);
// --link-decoration: var(--link-hover-decoration, var(--link-decoration));
- color: var(--theme-text-emphasis, var(--link-hover-color));
+ color: var(--theme-fg-emphasis, var(--link-hover-color));
text-decoration: var(--link-hover-decoration, var(--link-decoration));
}
}
> :not(caption) > * > * {
padding: var(--table-cell-padding-y) var(--table-cell-padding-x);
// Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb
- color: var(--table-color-state, var(--table-color-type, var(--theme-text, var(--table-color))));
+ color: var(--table-color-state, var(--table-color-type, var(--theme-fg, var(--table-color))));
background-color: var(--theme-bg-subtle, var(--table-bg));
border-block-end-width: var(--table-border-width);
box-shadow: inset 0 0 0 9999px var(--table-bg-state, var(--table-bg-type, var(--theme-bg-subtle, var(--table-accent-bg))));
// For rows
.table-striped {
> tbody > tr:nth-of-type(#{$table-striped-order}) > * {
- --table-color-type: var(--theme-text, var(--table-striped-color));
- --table-bg-type: color-mix(in srgb, var(--theme-text, var(--table-color)) var(--table-striped-bg-factor), transparent);
+ --table-color-type: var(--theme-fg, var(--table-striped-color));
+ --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent);
}
}
// For columns
.table-striped-columns {
> :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) {
- --table-color-type: var(--theme-text, var(--table-striped-color));
- --table-bg-type: color-mix(in srgb, var(--theme-text, var(--table-color)) var(--table-striped-bg-factor), transparent);
+ --table-color-type: var(--theme-fg, var(--table-striped-color));
+ --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent);
}
}
// The `.table-active` class can be added to highlight rows or cells
.table-active {
- --table-color-state: var(--theme-text, var(--table-active-color));
- --table-bg-state: color-mix(in srgb, var(--theme-text, var(--table-color)) var(--table-active-bg-factor), transparent);
+ --table-color-state: var(--theme-fg, var(--table-active-color));
+ --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-active-bg-factor), transparent);
}
// Hover effect
.table-hover {
> tbody > tr:hover > * {
- --table-color-state: var(--theme-text, var(--table-hover-color));
- --table-bg-state: color-mix(in srgb, var(--theme-text, var(--table-color)) var(--table-hover-bg-factor), transparent);
+ --table-color-state: var(--theme-fg, var(--table-hover-color));
+ --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-hover-bg-factor), transparent);
}
}
// @each $color-name, $theme-props in $theme-map {
// .chip-input.theme-#{$color-name} > .chip {
// // Subtle default state
- // --chip-color: var(--theme-text);
+ // --chip-color: var(--theme-fg);
// --chip-bg: var(--theme-bg-subtle);
// // Selected/active solid state
$form-feedback-margin-top: .5rem !default;
$form-feedback-font-size: var(--font-size-xs) !default;
$form-feedback-font-style: null !default;
-$form-feedback-valid-color: var(--success-text) !default;
-$form-feedback-invalid-color: var(--danger-text) !default;
+$form-feedback-valid-color: var(--success-fg) !default;
+$form-feedback-invalid-color: var(--danger-fg) !default;
$form-feedback-icon-valid-color: #00a748 !default;
$form-feedback-icon-valid: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'><path fill='#{$form-feedback-icon-valid-color}' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/></svg>") !default;
--radio-bg: var(--radio-disabled-bg);
~ label {
- color: var(--secondary-text);
+ color: var(--secondary-fg);
cursor: default;
}
}
&::before { opacity: .4; }
~ label {
- color: var(--secondary-text);
+ color: var(--secondary-fg);
cursor: default;
}
}
@include output() {
@include color-mode(dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
}
@include expect() {
[data-bs-theme=dark] .element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
[data-bs-theme=dark] {
@include output() {
@include color-mode(dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
@include expect() {
@media (prefers-color-scheme: dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
--alert-bg: var(--theme-bg-subtle, var(--bg-1)),
--alert-padding-x: 1rem,
--alert-padding-y: 1rem,
- --alert-color: var(--theme-text, inherit),
+ --alert-color: var(--theme-fg, inherit),
--alert-border-color: var(--theme-border, var(--border-color)),
--alert-border: var(--border-width) solid var(--alert-border-color),
--alert-border-radius: var(--border-radius),
Cards are flexible and extensible content containers. They include options for headers and footers, a wide variety of content, contextual background colors, and powerful display options. **Cards have no fixed width to start**, so they’ll naturally fill the full width of its parent element, or slot into your grid columns. This is easily customized with our various [sizing options](#width).
<Example code={`<section class="card" style="width: 280px">
+ <Placeholder width="100%" height="180" class="card-img-top" text="Image cap" />
<div class="card-body">
<h4 class="card-title">Card title</h4>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card’s content.</p>
<a href="#" class="btn-solid theme-primary">Go somewhere</a>
</div>
- <Placeholder width="100%" height="180" class="card-img-top" text="Image cap" />
</section>`} />
## How it works
Turn an image into a card background and overlay your card’s text. Depending on the image, you may or may not need additional styles or utilities.
-<Example code={`<div class="card text-bg-dark">
+<Example code={`<div class="card theme-inverse">
<Placeholder width="100%" height="270" class="bd-placeholder-img-lg card-img" text="Card image" />
<div class="card-img-overlay">
<h4 class="card-title">Card title</h4>
@include color-mode(dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
```css
[data-bs-theme=dark] .element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
```
@include color-mode(dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
```css
@media (prefers-color-scheme: dark) {
.element {
- color: var(--bs-primary-text-emphasis);
+ color: var(--bs-primary-fg-emphasis);
background-color: var(--bs-primary-bg-subtle);
}
}
<ScssDocs name="custom-color-mode" file="site/src/scss/_content.scss" />
-<Example showMarkup={false} code={`<div data-bs-theme="blue">
- <div class="bd-example fg-body bg-body">
+<Example showMarkup={false} code={`<div class="p-5 fg-body bg-body rounded-3" data-bs-theme="blue">
<div class="h4">Example blue theme</div>
<p>Some paragraph text to show how the blue theme might look with written copy.</p>
<a class="menu-item" href="#">Separated link</a>
</div>
</div>
- </div>
</div>`} />
```html
export const themeTokens = [
'base',
- 'text',
- 'text-emphasis',
+ 'fg',
+ 'fg-emphasis',
'bg',
'bg-subtle',
'bg-muted',
| Theme token | Description |
| --- | --- |
| `base` | The default color value for the semantic color |
-| `text` | Accessible text color (against body, plus `subtle` and `muted` color tokens) |
-| `text-emphasis` | Emphasized text color for use with `muted` background tokens |
+| `fg` | Accessible text color (against body, plus `subtle` and `muted` color tokens) |
+| `fg-emphasis` | Emphasized text color for use with `muted` background tokens |
| `bg` | For solid colored backgrounds with high contrast |
-| `bg-subtle` | Lowest contrast backgrounds, usually paired with `text` key for text color |
+| `bg-subtle` | Lowest contrast backgrounds, usually paired with `fg` key for text color |
| `bg-muted` | Lower contrast backgrounds, often used for disabled states |
| `border` | Borders and dividers |
| `focus-ring` | For visible focus indicators and outline styles |
Not all colors can be used for all purposes due to accessibility, color contrast, and general aesthetic considerations. Here are some recommended pairings.
- `contrast` work best with `bg`
-- `text` works best with `subtle`
-- `text-emphasis` works best with `muted`
+- `fg` works best with `subtle`
+- `fg-emphasis` works best with `muted`
- `border` works best with `subtle`, but can be used with `muted`
<Example code={`<div class="theme-primary vstack gap-3">
<div style="color: var(--bs-theme-contrast); background: var(--bs-theme-bg);" class="p-3 rounded-3">
Example element with theme color
</div>
- <div style="color: var(--bs-theme-text-emphasis); background: var(--bs-theme-bg-muted); border: 1px solid var(--bs-theme-border);" class="p-3 rounded-3">
+ <div style="color: var(--bs-theme-fg-emphasis); background: var(--bs-theme-bg-muted); border: 1px solid var(--bs-theme-border);" class="p-3 rounded-3">
Example element with theme color
</div>
- <div style="color: var(--bs-theme-text); background: var(--bs-theme-bg-subtle); border: 1px solid var(--bs-theme-border);" class="p-3 rounded-3">
+ <div style="color: var(--bs-theme-fg); background: var(--bs-theme-bg-subtle); border: 1px solid var(--bs-theme-border);" class="p-3 rounded-3">
Example element with theme color
</div>
</div>`} />
</div>
</div>`} />
-Under the hood, token values like `--bs-primary-text` resolve differently depending on the active color mode. For example, in our `_theme.scss`, the primary text color is defined as:
+Under the hood, token values like `--bs-primary-fg` resolve differently depending on the active color mode. For example, in our `_theme.scss`, the primary text color is defined as:
```scss
-"text": light-dark(var(--blue-600), var(--blue-400)),
+"fg": light-dark(var(--blue-600), var(--blue-400)),
```
When `data-bs-theme="light"` is active (or the user's system preference is light), this resolves to `var(--blue-600)`. In dark mode, it resolves to `var(--blue-400)`—a lighter shade that maintains readability against dark backgrounds. The same pattern applies to backgrounds, borders, and all other adaptive tokens.
## Sass & CSS
-Bootstrap 6 has a new philosophy of using Sass and CSS together to customize the project for your needs. At a high level, **Sass is for programmatic customization** and **CSS variables are for visual customization**. Previous iterations have treated Sass as both programmatic and visual customization with CSS variables adding a more complicated secondary layer. This has been greatly improved in v6.
+Bootstrap 6 brings with it a new philosophy of using Sass and CSS together to build and customize projects for your specific needs. At a high level:
+
+- **Sass is for programmatic configuration —** Think functions, loops, mixins, and more for generative constructs.
+- **CSS is for visual customization —** CSS variables for visual properties like colors, spacing, typography, etc.
+
+Previous major versions have treated Sass as both programmatic and visual customization with CSS variables adding a more complicated, and less complete, secondary layer. This has been greatly improved in v6.
+
+On top of that, we use [PostCSS](https://postcss.org/) to prefix our CSS variables with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate.
### Sass
### Examples
-Here's how we'd use a mix of Sass and CSS to customize Bootstrap. The Sass helps us manage features and gives us access to generative tokens, while the CSS allows us to customize individual tokens and values.
+Here's how we'd use a mix of Sass and CSS to customize Bootstrap. The Sass helps us manage features and gives us access to generative tokens, while the CSS allows us to customize individual tokens and values. In almost all our components, we setup a Sass map of "tokens" that are really just CSS variables. Using this map, we then generate the CSS custom properties on the component's class.
+
+For example, the alert component has a `$alert-tokens` map that is used to generate the CSS custom properties on the `.alert` class.
+
+```scss
+$alert-tokens: (
+ --alert-padding-x: 2rem,
+ --alert-border-radius: 1rem,
+ // …
+);
+
+.alert {
+ @include tokens($alert-tokens);
+ // …
+}
+```
+
+In practice for you and your projects, this means you can use Sass's module system to override the CSS variables at build-time for a specific component, or even globally.
```scss
@use "../node_modules/bootstrap/scss/bootstrap" with (
### Responsive design
-Responsive web design is the practice of building a website that responds to the viewport size of the device it's being viewed on. This is achieved by using media queries to apply different styles to the website based on the viewport size.
+Responsive web design is the practice of building a website that responds to the viewport size of the device it's being viewed on. This is achieved by using range media queries to apply different styles to the website based on the viewport size.
```css
-@media (max-width: 768px) {
+@media (width < 768px) {
.container {
max-width: 100%;
}
Bootstrap requires the use of the HTML5 doctype. Without it, you’ll see some funky and incomplete styling.
```html
-<!doctype html>
+<!doctype html><!--[!code highlight]-->
<html lang="en">
- ...
+ …
</html>
```
Bootstrap is developed *mobile first*, a strategy in which we optimize code for mobile devices first and then scale up components as necessary using CSS media queries. To ensure proper rendering and touch zooming for all devices, add the responsive viewport meta tag to your `<head>`.
```html
-<meta name="viewport" content="width=device-width, initial-scale=1">
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1"><!--[!code highlight]-->
+ …
+ </head>
+ <body>
+ …
+ </body>
+</html>
```
You can see an example of this in action in [the quick start]([[docsref:/guides/quickstart/]]).
### Browser support
-You can find our supported range of browsers and their versions [in our `.browserslistrc file`]([[config:repo]]/blob/v[[config:current_version]]/.browserslistrc):
+You can find our supported range of browsers and their versions [in our `.browserslistrc` file]([[config:repo]]/blob/v[[config:current_version]]/.browserslistrc):
<Code lang="plaintext" filePath=".browserslistrc" />
- Use utilities over custom styles
- Avoid enforcing strict HTML requirements (immediate children selectors)
+These rules cannot always be followed to the letter, but we strive to follow them as much as possible.
+
### Responsive
Bootstrap's styles are mobile-first—we add styles as the viewport grows rather than overriding them as it shrinks. Not every component must be fully responsive, but this approach reduces CSS overrides.
Components built with overlays also have a predefined z-index scale, beginning at `1000`. This starting number was chosen arbitrarily and serves as a small buffer between our styles and your project’s custom styles.
-<ScssDocs name="zindex-stack" file="scss/_config.scss" />
+<ScssDocs name="zindex-levels-map" file="scss/_config.scss" />
Each overlay component increases its `z-index` value slightly in such a way that common UI principles allow user focused or hovered elements to remain in view at all times. For example, a modal is document blocking (e.g., you cannot take any other action save for the modal’s action), so we put that above our navbars.
## Install
-Pull in Bootstrap’s **source files** into nearly any project with some of the most popular package managers. No matter the package manager, Bootstrap will **require a [Sass compiler]([[docsref:/guides/contribute#sass]]) and [Autoprefixer](https://github.com/postcss/autoprefixer)** for a setup that matches our official compiled versions.
+Pull in Bootstrap’s **source files** into nearly any project with some of the most popular package managers. No matter the package manager, Bootstrap will **require a [Sass compiler]([[docsref:/guides/contribute#sass]]) and [PostCSS](https://github.com/postcss/postcss)** for a setup that matches our official compiled versions.
Package managed installs don't include documentation or our full build scripts. You can also [use any demo from our Examples repo](https://github.com/twbs/examples) to quickly jumpstart Bootstrap projects.
+After installing, head to [our npm guide]([[docsref:/guides/npm]]) for a full setup guide.
+
<Code
tabs={[
{ label: 'npm', code: 'npm install bootstrap', lang: 'bash' },
### CSS
+- **Clarified and simplified CSS-Sass setup.** Read more about our [approach]([[docsref:/getting-started/approach]]) for how we use Sass and CSS together to build and customize projects for your specific needs, in particular how we use CSS variables inside Sass maps as our first-class customization layer.
- **Implemented CSS layers** in `_root.scss` and applied them to all our Sass files.
- Layers are set in `_root.scss` and then utilized across separate Sass partials.
- We cannot, unfortunately, wrap `@use` or `@forward` statements in `@layer`—Sass expects those to be top level at all times. Also, while CSS allows `@import "file.css" layer(name)`, Sass also does not support that.
-- Clarified and simplified CSS-Sass setup.
- New, streamlined color modes and theming.
- Removed `_maps.scss`
- Removed `_variables-dark.scss`
- Renamed `xxl` to `2xl` for better scaling with additional custom breakpoints
- Increased the `2xl` breakpoint from 1400px to 1536px, and it's container from 1320px to 1440px.
- **Adopted modern CSS color functions.** All Sass color variables now use `oklch()` notation (e.g., `$blue: oklch(60% 0.24 240)`) and tint/shade scales are generated with `color-mix(in lab, ...)` in the compiled CSS. The v5 `$*-rgb` CSS custom properties and `rgba()` patterns have been removed. This requires browser support for `color-mix()` and `oklch()`.
-- **New theme token system with `.theme-*` classes.** Per-component color variant classes (like `.alert-primary`, `.badge.bg-primary`, `.btn-primary`) are replaced by a composable `.theme-{name}` pattern. Adding `.theme-primary` to a component sets `--theme-bg`, `--theme-text`, `--theme-border`, `--theme-contrast`, and other semantic CSS custom properties that the component reads. This applies across buttons, badges, alerts, cards, accordions, and more.
+- **New theme token system with `.theme-*` classes.** Per-component color variant classes (like `.alert-primary`, `.badge.bg-primary`, `.btn-primary`) are replaced by a composable `.theme-{name}` pattern. Adding `.theme-primary` to a component sets `--theme-bg`, `--theme-fg`, `--theme-border`, `--theme-contrast`, and other semantic CSS custom properties that the component reads. This applies across buttons, badges, alerts, cards, accordions, and more.
- **Responsive and state classes now use a prefix instead of an infix or suffix.** Class names follow the Tailwind-style `prefix:class` pattern (e.g., `md:d-none` instead of `d-md-none`, `hover:opacity-50` instead of `opacity-50-hover`). In HTML, use the unescaped colon: `class="md:d-none"`. This applies to utilities, grid, pseudo-state variants, and all responsive components.
<BsTable>
class: striped,
child-selector: "> :nth-child(odd)",
values: (
- null: var(--bs-tertiary-bg),
+ null: var(--bs-bg-1),
)
)
);
Output:
```css
-:where(.striped > :nth-child(odd)) { background-color: var(--bs-tertiary-bg); }
+:where(.striped > :nth-child(odd)) { background-color: var(--bs-bg-1); }
```
Or target all direct children with `> *`:
## Colors
-Change the `background-color` using utilities built on our theme colors using `.bg-{color}`, `.bg-muted-{color}`, and `.bg-subtle-{color}`. All these background color utilities set the `background-color` property to a local CSS variable, `--bs-bg`, which has the value of the theme color. Most color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
+Change the `background-color` using utilities built on our theme colors using `.bg-{color}`, `.bg-muted-{color}`, and `.bg-subtle-{color}`. All these background color utilities set the `background-color` property to a local CSS variable, `--bs-bg`, which has the value of the theme color. The root color tokens also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
-For example, `.bg-primary` sets the `--bs-bg` variable to `var(--bs-primary-500)`:
+For example, `.bg-primary` sets the `--bs-bg` variable to `var(--bs-primary-bg)`:
```css
.bg-primary {
- --bs-bg: var(--bs-primary-500);
+ --bs-bg: var(--bs-primary-bg);
background-color: var(--bs-bg);
}
```
<div class="bg-success p-2 text-dark bg-20">This is 20% opacity success background</div>
<div class="bg-success p-2 text-dark bg-10">This is 10% opacity success background</div>`} />
+The same can be applied to subtle and muted background colors.
+
+<Example class="d-flex flex-column gap-2" code={`<div class="bg-subtle-success p-2 text-success">This is subtle success background</div>
+<div class="bg-muted-success bg-40 p-2 text-success">This is 40% success background</div>
+<div class="bg-muted-success bg-20 p-2 text-success">This is 20% success background</div>
+`} />
+
## CSS
In addition to the following Sass functionality, consider reading about our included [CSS custom properties]([[docsref:/getting-started/css-variables]]) (aka CSS variables) for colors and more.
## Colors
-Change the `border-color` using utilities built on our theme colors using `.border-{color}` and `.border-subtle-{color}`. All these border color utilities set the `border-color` property to a local CSS variable, `--bs-border-color`, which has the value of the theme color. Color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
+Change the `border-color` using utilities built on our theme colors using `.border-{color}` and `.border-subtle-{color}`. All these border color utilities set the `border-color` property to a local CSS variable, `--bs-border-color`, which has the value of the theme color. The root color tokens color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
-For example, `.border-primary` sets the `--bs-border-color` variable to `var(--bs-blue-500)`:
+For example, `.border-primary` sets the `--bs-border-color` variable to `var(--bs-primary-border)`:
```css
.border-primary {
- --bs-border-color: var(--bs-blue-500);
+ --bs-border-color: var(--bs-primary-border);
border-color: var(--bs-border-color);
}
```
This approach allows us to also easily support translucency with our `.border-{opacity}` utilities as we can use `color-mix()` with the CSS variable to generate the appropriate color. See the [opacity section](#opacity) for more details.
<Example class="d-flex flex-column gap-2 bd-example-border-color-utils" code={[
- ...getData('theme-colors').map((themeColor) => `<span class="border border-5 border-${themeColor.name}">${themeColor.name}</span>
-<span class="border border-5 border-subtle-${themeColor.name}">${themeColor.name} subtle</span>`),
- `<span class="border border-5 border-bg">bg</span>
- <span class="border border-5 border-body">body</span>
- <span class="border border-5 border-muted">muted</span>
- <span class="border border-5 border-subtle">subtle</span>
- <span class="border border-5 border-emphasized">emphasized</span>
- <span class="border border-5 border-black">black</span>
-<span class="border border-5 border-white">white</span>`
+ ...getData('theme-colors').map((themeColor) => `<div class="border border-5 border-${themeColor.name}">${themeColor.name}</div>
+<div class="border border-5 border-subtle-${themeColor.name}">${themeColor.name} subtle</div>`),
+ `<div class="border border-5 border-bg">bg</div>
+ <div class="border border-5 border-body">body</div>
+ <div class="border border-5 border-muted">muted</div>
+ <div class="border border-5 border-subtle">subtle</div>
+ <div class="border border-5 border-emphasized">emphasized</div>
+ <div class="border border-5 border-black">black</div>
+<div class="border border-5 border-white">white</div>`
]} />
Or modify the default `border-color` of a component:
## Colors
-Change the `color` using utilities built on our theme colors using `.fg-{color}` and `.fg-emphasis-{color}`. All these color utilities set the `color` property to a local CSS variable, `--bs-fg`, which has the value of the theme color. Color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
+Change the `color` using utilities built on our theme colors using `.fg-{color}` and `.fg-emphasis-{color}`. All these color utilities set the `color` property to a local CSS variable, `--bs-fg`, which has the value of the theme color. The root color tokens color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
-For example, `.fg-primary` sets the `--bs-fg` variable to `light-dark(var(--bs-blue-600), var(--bs-blue-400))`:
+For example, `.fg-primary` sets the `--bs-fg` variable to `var(--bs-primary-fg)`:
```css
.fg-primary {
- --bs-fg: light-dark(var(--bs-blue-600), var(--bs-blue-400));
+ --bs-fg: var(--bs-primary-fg);
color: var(--bs-fg);
}
```
.theme-subtle {
background-color: var(--theme-bg-subtle);
- color: var(--theme-text);
+ color: var(--theme-fg);
}
.theme-muted {
background-color: var(--theme-bg-muted);
- color: var(--theme-text-emphasis);
+ color: var(--theme-fg-emphasis);
}
```
@media (min-width: 768px) {
.bd-placeholder-img-lg {
- font-size: 3.5rem;
+ font-size: 2.5rem;
}
}
const optionsWithDefaults = Object.assign(
{},
{
- background: '#adb5bd',
- color: '#e9ecef',
+ background: 'var(--bs-bg-2)',
+ color: 'var(--bs-fg-4)',
height: '180',
markup: 'svg',
title: 'Placeholder',
const textColor = color.replace(/^#/, '')
// Build the raw SVG string first
- let svg = `<svg style='font-size: 1.125rem; font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; -webkit-user-select: none; -moz-user-select: none; user-select: none; text-anchor: middle;' width='200' height='200' xmlns='http://www.w3.org/2000/svg'>`
+ let svg = `<svg style='font-size: 1.125rem; font-family:system-ui; -webkit-user-select: none; -moz-user-select: none; user-select: none; text-anchor: middle;' width='200' height='200' xmlns='http://www.w3.org/2000/svg'>`
if (showTitle) {
svg += `<title>${title}</title>`
padding: .25rem .75rem;
font-size: .8125rem;
line-height: 1.25rem;
- color: var(--bs-theme-text, var(--bs-fg-2));
+ color: var(--bs-theme-fg, var(--bs-fg-2));
background-color: var(--bs-theme-bg-subtle, var(--bs-gray-100));
@include border-radius(var(--bs-border-radius));
}
align-items: center;
justify-content: center;
margin-bottom: 1rem;
- color: var(--bs-secondary-color);
- background-color: var(--bs-tertiary-bg);
+ color: var(--bs-fg-3);
+ background-color: var(--bs-bg-1);
border: var(--bs-border-width) solid var(--bs-border-color);
@include border-radius(var(--bs-border-radius));
}
width: 5rem;
height: 5rem;
margin: .25rem;
- background-color: var(--bs-tertiary-bg);
+ background-color: var(--bs-bg-2);
}
}
margin-bottom: .25rem;
// stylelint-disable selector-max-type, selector-max-compound-selectors
- > p {
+ > p:not(:first-child) {
margin-block: 1rem;
}
--bs-table-border-color: var(--bs-border-color);
max-width: 100%;
- margin-bottom: 1.5rem;
- font-size: .875rem;
+ font-size: var(--bs-font-size-sm);
@include media-breakpoint-down(lg) {
&.table-bordered {
}
th {
- color: var(--bs-emphasis-color);
+ color: var(--bs-fg-body);
border-block-end-color: currentcolor;
}
&:not(.bd-callout) > strong { // Keep callout correct color when used within tables
- color: var(--bs-emphasis-color);
+ color: var(--bs-fg-body);
}
// Prevent breaking of code
}
// stylelint-enable selector-no-qualifying-type
- // stylelint-disable-next-line selector-no-qualifying-type
- rect[fill="#adb5bd"] {
- fill: var(--bs-bg-2);
- }
- // stylelint-disable-next-line selector-no-qualifying-type
- text[fill="#e9ecef"] {
- fill: var(--bs-fg-4);
- }
-
// scss-docs-start custom-color-mode
[data-bs-theme="blue"] {
- --fg-body: var(--white);
- --bg-body: var(--blue);
- --bg-3: var(--blue-600);
+ --bs-fg-body: var(--bs-white);
+ --bs-bg-body: var(--bs-blue-500);
+ --bs-bg-3: var(--bs-blue-600);
+ --bs-hr-border-color: var(--bs-blue-400);
.menu {
- --menu-bg: color-mix(in lab, var(--blue-500), var(--blue-600));
- --menu-item-active-bg: var(--blue-700);
+ --bs-menu-bg: color-mix(in lab, var(--bs-blue-600), var(--bs-blue-700));
+ --bs-menu-item-hover-bg: var(--bs-blue-700);
+ --bs-menu-item-active-bg: var(--bs-blue-800);
}
- .btn-secondary {
- --btn-bg: color-mix(in lab, var(--gray-600), var(--blue-400));
- --btn-border-color: color-mix(in lab, var(--fg-body) 25%, transparent);
- --btn-hover-bg: color-mix(in lab, var(--gray-600), var(--blue-400));
- --btn-hover-border-color: color-mix(in lab, var(--fg-body) 25%, transparent);
- --btn-active-bg: color-mix(in lab, color-mix(in lab, var(--gray-600), var(--blue-400)), var(--black) 10%);
- --btn-active-border-color: color-mix(in lab, var(--fg-body), transparent);
- --btn-focus-border-color: color-mix(in lab, var(--fg-body), transparent);
- --btn-focus-box-shadow: 0 0 0 .25rem rgb(255 255 255 / .2);
+ .btn-solid {
+ --bs-btn-bg: color-mix(in lab, var(--bs-gray-600), var(--bs-blue-400));
+ --bs-btn-border-color: color-mix(in lab, var(--bs-fg-body) 25%, transparent);
+ --bs-btn-hover-bg: color-mix(in lab, var(--bs-gray-600), var(--bs-blue-400));
+ --bs-btn-hover-border-color: color-mix(in lab, var(--bs-fg-body) 25%, transparent);
+ --bs-btn-active-bg: color-mix(in lab, color-mix(in lab, var(--bs-gray-600), var(--bs-blue-400)), var(--bs-black) 10%);
+ --bs-btn-active-border-color: color-mix(in lab, var(--bs-fg-body), transparent);
+ --bs-btn-focus-border-color: color-mix(in lab, var(--bs-fg-body), transparent);
+ --bs-btn-focus-box-shadow: 0 0 0 .25rem rgb(255 255 255 / .2);
}
}
// scss-docs-end custom-color-mode
// stylelint-enable
h1 {
- --bs-heading-color: var(--bs-emphasis-color);
- font-size: var(--font-size-4xl);
+ --bs-heading-color: var(--bs-fg-body);
+ font-size: var(--bs-font-size-4xl);
}
.lead {
font-size: 1.125rem;
font-weight: 400;
- color: var(--bs-secondary-color);
+ color: var(--bs-fg-3);
}
.bd-code-snippet {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- background-color: color-mix(in oklch, var(--body-color) #{math.percentage(.075)}, transparent);
+ background-color: color-mix(in oklch, var(--bs-fg-body) #{math.percentage(.075)}, transparent);
@include border-radius(calc(.5rem - 1px));
@include media-breakpoint-up(lg) {
h2,
h3,
h4 {
- --bs-heading-color: var(--bs-emphasis-color);
+ --bs-heading-color: var(--bs-fg-body);
}
.lead {
--menu-min-width: 12rem;
--menu-padding-x: .25rem;
--menu-padding-y: .25rem;
- --menu-item-hover-color: var(--bs-accent-text-emphasis);
+ --menu-item-hover-color: var(--bs-accent-fg-emphasis);
--menu-item-hover-bg: var(--bs-accent-bg-subtle);
--menu-item-active-bg: var(--bs-accent-bg);
}
.bd-placeholder-img-lg {
- font-size: 3.5rem;
+ font-size: 2.5rem;
}
}
}
}
-// .DocSearch-Button {
-// --docsearch-searchbox-background: transparent;
-// --docsearch-searchbox-color: var(--bs-color-body);
-// --docsearch-searchbox-focus-background: var(--bs-bg-2);
-// --docsearch-searchbox-shadow: none;
-// --docsearch-text-color: var(--bs-color-body);
-// --docsearch-muted-color: var(--bs-fg-4);
-
-// @include media-breakpoint-up(lg) {
-// display: flex;
-// align-items: center;
-// justify-content: center;
-// width: 100%;
-// min-width: 38px;
-// height: 38px;
-// padding-inline: 12px;
-// margin-left: 0;
-// font-weight: var(--bs-font-weight-normal);
-// color: var(--bs-fg-body);
-// // border: 1px solid var(--bs-border-subtle);
-// @include border-radius(var(--bs-border-radius));
-
-// .DocSearch-Search-Icon {
-// width: 16px;
-// height: 16px;
-// }
-
-// &:active,
-// &:focus,
-// &:hover {
-// .DocSearch-Search-Icon {
-// opacity: 1;
-// }
-// }
-
-// @include media-breakpoint-up(md) {
-// justify-content: space-between;
-// color: var(--bs-fg-3);
-
-// .DocSearch-Search-Icon {
-// opacity: .5;
-// }
-// }
-// }
-
-// .DocSearch-Button-Keys,
-// .DocSearch-Button-Placeholder {
-// @include media-breakpoint-down(lg) {
-// display: none;
-// }
-// }
-
-// .DocSearch-Button-Placeholder {
-// font-size: 14px;
-// }
-
-// .DocSearch-Button-Keys {
-// display: flex;
-// gap: 2px;
-// align-items: center;
-// min-width: 0;
-// margin-top: .25rem;
-// }
-
-// .DocSearch-Button-Key {
-// position: static;
-// top: 0;
-// width: auto;
-// margin: 0;
-// font-size: var(--bs-font-size-sm);
-// background: none;
-// border-radius: 0; // stylelint-disable-line
-// box-shadow: none;
-// }
-
-// .DocSearch-Commands-Key {
-// background-image: none;
-// box-shadow: none;
-// }
-
.DocSearch-Form {
@include border-radius(var(--bs-border-radius));
}
.bd-links-heading {
gap: .25rem;
- color: var(--bs-emphasis-color);
+ color: var(--bs-fg-body);
}
.bd-links-heading .bi {
width: 16px;
display: flex;
flex-direction: column;
width: fit-content;
+ min-width: 100%;
.line:empty {
min-height: 1lh;