diff --git a/site/src/components/shortcodes/TokenPlayground.astro b/site/src/components/shortcodes/TokenPlayground.astro
new file mode 100644
index 000000000000..cc1ac5bce2bb
--- /dev/null
+++ b/site/src/components/shortcodes/TokenPlayground.astro
@@ -0,0 +1,358 @@
+---
+import Code from '@components/shortcodes/Code.astro'
+
+// Both menus move two steps of their scale rather than one, which is what
+// changing the Sass base value does: `--radius-7` is `$radius * 1.5` and
+// `--spacer-6` is `$spacer * 1.25` in scss/_config.scss. The components below
+// read `--radius-5` and `--spacer` (button, input, alert) or `--radius-7` and
+// `--spacer-6` (card), so driving both makes the whole group move together.
+const defaultRadius = '0.5'
+const defaultSpacer = '1'
+const defaultColor = 'blue'
+
+const radiusOptions = ['0', '0.25', '0.5', '0.75', '1', '1.25', '1.5']
+const spacerOptions = ['0.75', '1', '1.25', '1.5']
+
+const menus = [
+ { control: 'radius', label: 'Radius', value: defaultRadius, options: radiusOptions },
+ { control: 'spacer', label: 'Spacer', value: defaultSpacer, options: spacerOptions }
+]
+
+const palette = [
+ { name: 'blue', label: 'Blue' },
+ { name: 'purple', label: 'Purple' },
+ { name: 'pink', label: 'Pink' },
+ { name: 'orange', label: 'Orange' },
+ { name: 'green', label: 'Green' }
+]
+
+// Today’s model: reminting `--bs-primary-base` alone recolors solid surfaces
+// (button, badge) but leaves alerts, borders, and focus rings on the compiled
+// blue pairing. To move the whole theme you must restate every derived token,
+// which is what this snippet and the script below do.
+const css = `.token-demo {
+ --bs-radius-5: ${defaultRadius}rem;
+ --bs-spacer: ${defaultSpacer}rem;
+ --bs-primary-base: var(--bs-${defaultColor}-500);
+ --bs-primary-bg: var(--bs-primary-base);
+ --bs-primary-bg-subtle: light-dark(var(--bs-${defaultColor}-100), var(--bs-${defaultColor}-900));
+ --bs-primary-fg: light-dark(var(--bs-${defaultColor}-600), var(--bs-${defaultColor}-400));
+ --bs-primary-border: light-dark(var(--bs-${defaultColor}-300), var(--bs-${defaultColor}-600));
+}`
+---
+
+
+
+
+ {
+ menus.map((menu) => (
+
+
+ {menu.label}
+
+
+
+ {menu.options.map((option) => (
+
+ ))}
+
+
+ ))
+ }
+
+
+ Primary
+
+ {
+ palette.map((color) => (
+
+ ))
+ }
+
+
+
+
+ Remint
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A token change reaches every component at once.
+
+
+
Card header
+
+
Card
+
Padding follows the spacer, corners follow the radius.
+
+
+
+
+
+
+
+
+
+
diff --git a/site/src/components/shortcodes/index.ts b/site/src/components/shortcodes/index.ts
index 1bbf86b949c0..1165cd4a2131 100644
--- a/site/src/components/shortcodes/index.ts
+++ b/site/src/components/shortcodes/index.ts
@@ -24,3 +24,4 @@ export { default as SpacingResponsive } from './SpacingResponsive.astro'
export { default as Swatch } from './Swatch.astro'
export { default as Table } from './Table.astro'
export { default as TableContent } from './TableContent.md'
+export { default as TokenPlayground } from './TokenPlayground.astro'
diff --git a/site/src/content/docs/customize/theme.mdx b/site/src/content/docs/customize/theme.mdx
index cd5a4e9303fb..c7ed92609216 100644
--- a/site/src/content/docs/customize/theme.mdx
+++ b/site/src/content/docs/customize/theme.mdx
@@ -53,6 +53,14 @@ And within each semantic theme color, you’ll find the following keys, most of
| `contrast` | Text color that needs to be readable on the `base` and `bg` colors |
+## Token playground
+
+Every component reads the same theme and layout tokens, so one change reaches the whole group. Change the radius, the spacer, or the primary color below and watch the button, input, alert, and card update together.
+
+
+
+The **Remint** control shows how the theme works today. Set it to **All** and each color restates every derived token—`bg-subtle`, `fg`, `border`, and the rest—so the whole preview follows. Set it to **Base** and only `--bs-primary-base` and `--bs-primary-bg` change. The solid button recolors, but the alert keeps the compiled blue pairing, because its `bg-subtle`, `fg`, and `border` tokens still point at the blue palette on `:root`.
+
## Theme colors
Every token is available as a CSS variable, and most are then consumed by our utilities and components. So for the `primary` color, you have the following colors:
diff --git a/site/src/scss/_component-examples.scss b/site/src/scss/_component-examples.scss
index d2d114302465..0228a7ec7fe2 100644
--- a/site/src/scss/_component-examples.scss
+++ b/site/src/scss/_component-examples.scss
@@ -426,4 +426,52 @@
grid-template-columns: repeat(9, 1fr);
}
}
+
+ //
+ // Token playground
+ //
+
+ .bd-playground {
+ --bd-example-border-color: var(--bs-border-subtle);
+
+ border: 1px solid var(--bd-example-border-color);
+ @include border-radius(var(--radius-5));
+
+ .bd-playground-controls {
+ border-bottom: 1px solid var(--bd-example-border-color);
+ @include border-top-radius(calc(var(--radius-5) - 1px));
+ }
+
+ .bd-code-snippet {
+ border: 0;
+ @include border-radius(0 0 calc(var(--radius-5) - 1px) calc(var(--radius-5) - 1px));
+ }
+
+ .menu-item.selected {
+ font-weight: var(--font-weight-semibold);
+ }
+ }
+
+ .bd-token-swatch {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.75rem;
+ height: 1.75rem;
+ padding: 0;
+ color: var(--bs-white);
+ cursor: pointer;
+ border: 1px solid color-mix(in srgb, var(--bs-fg-body) 15%, transparent);
+ @include border-radius(var(--radius-4));
+
+ svg {
+ width: 1rem;
+ height: 1rem;
+ opacity: 0;
+ }
+
+ &.selected svg {
+ opacity: 1;
+ }
+ }
}