---
title: Browser Support
description: Learn about the browser support for Bamboo
---

Bamboo CSS is built with modern CSS features and uses [PostCSS](https://postcss.org/) to add support for older browsers.
Bamboo supports the latest, stable releases of major browsers that support the following features:

- [CSS Variables](https://caniuse.com/css-variables)
- [CSS Cascade Layers](https://caniuse.com/css-cascade-layers)
- Modern selectors, such as [`:where()`](https://caniuse.com/mdn-css_selectors_where) and
  [`:is()`](https://caniuse.com/css-matches-pseudo)
- [`@scope`](https://caniuse.com/css-cascade-scope) for CLI/PostCSS slot-recipe extraction
- [`light-dark()`](https://caniuse.com/mdn-css_types_color_light-dark)

## Browserlist

Based on the above criteria, the following browsers are supported:

```txt
Chrome >= 123
Edge >= 123
Firefox >= 146
iOS >= 17.5
Safari >= 17.5
Android >= 123
Opera >= 109
```

These numbers describe Bamboo's complete output surface. [`light-dark()`](#semantic-tokens-and-light-dark) fixes most of
them; extraction-only slot recipes additionally depend on [`@scope`](#slot-recipes-and-scope-in-extraction-only-builds).
A Vite-compiled application emits no `@scope` for slot recipes, so that feature does not set its application baseline.

Bamboo reads this baseline when it minifies, so it is what decides whether a selector list may be folded into `:is()`.
It is deliberately fixed rather than taken from your `browserslist` config, so that the same source always compiles to
the same CSS.

## Semantic tokens and `light-dark()`

A semantic token with a `_osDark` value and nothing else conditional is emitted as one declaration rather than a
declaration plus a media block:

```css
:where(:root, :host) {
  color-scheme: light dark;
  --colors-bg: light-dark(var(--colors-white), var(--colors-black));
}
```

`color-scheme` is emitted with the tokens, not in the reset, because it is a prerequisite rather than a nicety —
`light-dark()` returns its light value whenever `color-scheme` does not name both, so a sheet without it is one where
dark mode silently never engages. Putting it in the reset would lose it to `preflight: false`. It sits at zero
specificity, so anything you set wins.

That is what makes an explicit theme toggle cheap. `_osDark` is a media query and a `[data-theme=dark]` selector is not,
so the two are separate mechanisms that resolve against each other by source order. `color-scheme` is an ordinary
inherited property, so a toggle is one declaration on a subtree rather than a second copy of every token:

```css
[data-theme='dark'] {
  color-scheme: dark;
}
```

Two shapes are deliberately left alone. A token that also carries `_osLight` keeps both media blocks — folding it would
put the light arm of `light-dark()` and an `@media (prefers-color-scheme: light)` block in play for the same variable,
where the block wins on order and the arm is dead. And `_dark` is a class selector rather than a media query, so it
stays a rule of its own; reach for `color-scheme` on that subtree instead of restating each token.

If you redefine the `osDark` condition to something that is not `@media (prefers-color-scheme: dark)` — a selector, say
— nothing folds, because `light-dark()` cannot express it.

## Slot recipes and `@scope` in extraction-only builds

The Vite compiler does not emit `@scope`: it returns the selected shared atoms directly for every slot. Consequently,
Vite slot recipes add no `@scope` requirement to the browser baseline.

CLI/PostCSS extraction is different. There, a [slot recipe](/docs/concepts/slot-recipes) that declares a slot named
`root` emits its non-root slots' variant styles inside an `@scope` rule, so that no slot has to be handed the variant at
runtime:

```css
@scope (.checkbox__root--size_md) to (.checkbox__root) {
  .checkbox__control {
    width: 10;
  }
}
```

That is the only place Bamboo emits `@scope`, and it sets the baseline above when using extraction-only output.

There is no polyfill, and there will not be one. `@scope` decides between two matching rules by DOM proximity, which a
build step cannot compute and a stylesheet cannot express — the whole point is that the nearest root wins over an outer
one. Anything claiming to polyfill it either ignores nesting or needs a runtime.

Extraction-only slot recipes accept
[`scopeRoots`](/docs/concepts/slot-recipes#variant-scoping-in-extraction-only-output), and `scopeRoots: []` emits no
`@scope`. That is a cost and layout control — it is what you reach for when a slot renders through a portal. Prefer the
Vite compiler when the application must avoid `@scope` altogether.

## Polyfills

In event that you need to support older browsers, you can use the following polyfills in your PostCSS config:

- [autoprefixer](https://github.com/postcss/autoprefixer): Adds vendor prefixes to CSS rules using values from
  [Can I Use](https://caniuse.com/).
- [postcss-cascade-layers](https://www.npmjs.com/package/@csstools/postcss-cascade-layers): Adds support for CSS Cascade
  Layers.

There is deliberately none listed for `@scope`; see above for why there cannot be one.

Here is an example of a `postcss.config.js` file that uses these polyfills:

```js
module.exports = {
  plugins: ['@bamboocss/dev/postcss', 'autoprefixer', '@csstools/postcss-cascade-layers'],
}
```
