Skip to main content
Bun’s bundler has built-in support for CSS with the following features:
  • Transpiling modern/future features to work on all browsers (including vendor prefixing)
  • Minification
  • CSS Modules
  • Tailwind (through a native bundler plugin)

Transpiling

Transpiling and vendor prefixing are enabled by default, so you can use modern and future CSS features without worrying about browser compatibility. Bun’s CSS parser and bundler is a direct port of LightningCSS, with a bundling approach inspired by esbuild. The transpiler converts modern CSS syntax into backwards-compatible equivalents that work across browsers.
Thanks to the authors of LightningCSS and esbuild for their work.

Browser Compatibility

By default, Bun’s CSS bundler targets the following browsers:
  • ES2020
  • Edge 88+
  • Firefox 78+
  • Chrome 87+
  • Safari 14+

Syntax Lowering

Nesting

With CSS Nesting, you write child styles directly inside their parent blocks instead of repeating parent selectors across your CSS file.
styles.css
/* With nesting */
.card {
  background: white;
  border-radius: 4px;

  .title {
    font-size: 1.2rem;
    font-weight: bold;
  }

  .content {
    padding: 1rem;
  }
}
Bun’s CSS bundler automatically converts this nested syntax into traditional flat CSS that works in all browsers:
styles.css
/* Compiled output */
.card {
  background: white;
  border-radius: 4px;
}

.card .title {
  font-size: 1.2rem;
  font-weight: bold;
}

.card .content {
  padding: 1rem;
}
You can also nest media queries and other at-rules inside selectors:
styles.css
.responsive-element {
  display: block;

  @media (min-width: 768px) {
    display: flex;
  }
}
This compiles to:
styles.css
.responsive-element {
  display: block;
}

@media (min-width: 768px) {
  .responsive-element {
    display: flex;
  }
}

Color mix

The color-mix() function blends two colors at a given ratio in a chosen color space. Use it to create color variations without calculating the resulting values yourself.
styles.css
.button {
  /* Mix blue and red in the RGB color space with a 30/70 proportion */
  background-color: color-mix(in srgb, blue 30%, red);

  /* Create a lighter variant for hover state */
  &:hover {
    background-color: color-mix(in srgb, blue 30%, red, white 20%);
  }
}
Bun’s CSS bundler evaluates these color mixes at build time when all color values are known (not CSS variables), generating static color values that work in all browsers:
styles.css
.button {
  /* Computed to the exact resulting color */
  background-color: #b31a1a;
}

.button:hover {
  background-color: #c54747;
}

Relative colors

Relative color syntax modifies individual components of an existing color. Adjust attributes like lightness, saturation, or individual channels without recalculating the entire color.
styles.css
.theme-color {
  /* Start with a base color and increase lightness by 15% */
  --accent: lch(from purple calc(l + 15%) c h);

  /* Take our brand blue and make a desaturated version */
  --subtle-blue: oklch(from var(--brand-blue) l calc(c * 0.8) h);
}
Bun’s CSS bundler computes these relative color modifications at build time (when not using CSS variables) and generates static color values for browser compatibility:
.theme-color {
  --accent: lch(69.32% 58.34 328.37);
  --subtle-blue: oklch(60.92% 0.112 240.01);
}
Use it for theme generation, accessible color variants, or color scales derived from a base color instead of hard-coding each value.

LAB colors

Modern CSS supports the perceptually uniform color spaces LAB, LCH, OKLAB, and OKLCH, which can represent colors outside the standard RGB gamut.
styles.css
.vibrant-element {
  /* A vibrant red that exceeds sRGB gamut boundaries */
  color: lab(55% 78 35);

  /* A smooth gradient using perceptual color space */
  background: linear-gradient(to right, oklch(65% 0.25 10deg), oklch(65% 0.25 250deg));
}
Bun’s CSS bundler converts these color formats to backwards-compatible alternatives for browsers that don’t support them:
styles.css
.vibrant-element {
  /* Fallback to closest RGB approximation */
  color: #ff0f52;
  /* P3 fallback for browsers with wider gamut support */
  color: color(display-p3 1 0.12 0.37);
  /* Original value preserved for browsers that support it */
  color: lab(55% 78 35);

  background: linear-gradient(to right, #cd4e15, #3887ab);
  background: linear-gradient(to right, oklch(65% 0.25 10deg), oklch(65% 0.25 250deg));
}

Color function

The color() function specifies colors in predefined color spaces beyond traditional RGB, giving you access to wider color gamuts.
styles.css
.vivid-element {
  /* Using the Display P3 color space for wider gamut colors */
  color: color(display-p3 1 0.1 0.3);

  /* Using A98 RGB color space */
  background-color: color(a98-rgb 0.44 0.5 0.37);
}
For browsers that don’t support these color spaces, Bun’s CSS bundler adds RGB fallbacks:
styles.css
.vivid-element {
  /* RGB fallback first for maximum compatibility */
  color: #fa1a4c;
  /* Keep original for browsers that support it */
  color: color(display-p3 1 0.1 0.3);

  background-color: #6a805d;
  background-color: color(a98-rgb 0.44 0.5 0.37);
}

HWB colors

The HWB (Hue, Whiteness, Blackness) color model expresses colors based on how much white or black is mixed with a pure hue. This makes tints and shades more direct to create than with RGB or HSL values.
styles.css
.easy-theming {
  /* Pure cyan with no white or black added */
  --primary: hwb(180 0% 0%);

  /* Same hue, but with 20% white added (tint) */
  --primary-light: hwb(180 20% 0%);

  /* Same hue, but with 30% black added (shade) */
  --primary-dark: hwb(180 0% 30%);

  /* Muted version with both white and black added */
  --primary-muted: hwb(180 30% 20%);
}
Bun’s CSS bundler converts HWB colors to RGB for compatibility with all browsers:
styles.css
.easy-theming {
  --primary: #00ffff;
  --primary-light: #33ffff;
  --primary-dark: #00b3b3;
  --primary-muted: #339999;
}

Color notation

Modern CSS supports space-separated RGB and HSL values (no commas) and hex colors with an alpha channel.
styles.css
.modern-styling {
  /* Space-separated RGB notation (no commas) */
  color: rgb(50 100 200);

  /* Space-separated RGB with alpha */
  border-color: rgba(100 50 200 / 75%);

  /* Hex with alpha channel (8 digits) */
  background-color: #00aaff80;

  /* HSL with simplified notation */
  box-shadow: 0 5px 10px hsl(200 50% 30% / 40%);
}
Bun’s CSS bundler converts these formats for older browsers:
styles.css
.modern-styling {
  /* Converted to comma format for older browsers */
  color: rgb(50, 100, 200);

  /* Alpha channels handled appropriately */
  border-color: rgba(100, 50, 200, 0.75);

  /* Hex+alpha converted to rgba when needed */
  background-color: rgba(0, 170, 255, 0.5);

  box-shadow: 0 5px 10px rgba(38, 115, 153, 0.4);
}

light-dark() color function

The light-dark() function takes two colors and applies one based on the current color scheme, so styles respect the user’s system preference without media queries.
styles.css
:root {
  /* Define color scheme support */
  color-scheme: light dark;
}

.themed-component {
  /* Automatically picks the right color based on system preference */
  background-color: light-dark(#ffffff, #121212);
  color: light-dark(#333333, #eeeeee);
  border-color: light-dark(#dddddd, #555555);
}

/* Override system preference when needed */
.light-theme {
  color-scheme: light;
}

.dark-theme {
  color-scheme: dark;
}
For browsers that don’t support light-dark(), Bun’s CSS bundler converts it to CSS variables with fallbacks:
styles.css
:root {
  --lightningcss-light: initial;
  --lightningcss-dark: ;
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --lightningcss-light: ;
    --lightningcss-dark: initial;
  }
}

.light-theme {
  --lightningcss-light: initial;
  --lightningcss-dark: ;
  color-scheme: light;
}

.dark-theme {
  --lightningcss-light: ;
  --lightningcss-dark: initial;
  color-scheme: dark;
}

.themed-component {
  background-color: var(--lightningcss-light, #ffffff) var(--lightningcss-dark, #121212);
  color: var(--lightningcss-light, #333333) var(--lightningcss-dark, #eeeeee);
  border-color: var(--lightningcss-light, #dddddd) var(--lightningcss-dark, #555555);
}

Logical properties

CSS logical properties define layout, spacing, and sizing relative to the document’s writing mode and text direction rather than physical screen directions, so layouts adapt to different writing systems.
styles.css
.multilingual-component {
  /* Margin that adapts to writing direction */
  margin-inline-start: 1rem;

  /* Padding that makes sense regardless of text direction */
  padding-block: 1rem 2rem;

  /* Border radius for the starting corner at the top */
  border-start-start-radius: 4px;

  /* Size that respects the writing mode */
  inline-size: 80%;
  block-size: auto;
}
For browsers that don’t fully support logical properties, Bun’s CSS bundler compiles them to physical properties for each text direction:
styles.css
/* For left-to-right languages */
.multilingual-component:dir(ltr) {
  margin-left: 1rem;
  padding-top: 1rem;
  padding-bottom: 2rem;
  border-top-left-radius: 4px;
  width: 80%;
  height: auto;
}

/* For right-to-left languages */
.multilingual-component:dir(rtl) {
  margin-right: 1rem;
  padding-top: 1rem;
  padding-bottom: 2rem;
  border-top-right-radius: 4px;
  width: 80%;
  height: auto;
}
If the :dir() selector isn’t supported, Bun generates additional fallbacks.

:dir() selector

The :dir() pseudo-class styles elements based on their text direction (RTL or LTR), as determined by the document or explicit direction attributes. Use it to write direction-aware styles without JavaScript.
styles.css
/* Apply different styles based on text direction */
.nav-arrow:dir(ltr) {
  transform: rotate(0deg);
}

.nav-arrow:dir(rtl) {
  transform: rotate(180deg);
}

/* Position elements based on text flow */
.sidebar:dir(ltr) {
  border-right: 1px solid #ddd;
}

.sidebar:dir(rtl) {
  border-left: 1px solid #ddd;
}
For browsers that don’t support the :dir() selector, Bun’s CSS bundler converts it to the more widely supported :lang() selector with appropriate language mappings:
styles.css
/* Converted to use language-based selectors as fallback */
.nav-arrow:lang(en, fr, de, es, it, pt, nl) {
  transform: rotate(0deg);
}

.nav-arrow:lang(ar, he, fa, ur) {
  transform: rotate(180deg);
}

.sidebar:lang(en, fr, de, es, it, pt, nl) {
  border-right: 1px solid #ddd;
}

.sidebar:lang(ar, he, fa, ur) {
  border-left: 1px solid #ddd;
}
If multiple arguments to :lang() aren’t supported, Bun generates further fallbacks.

:lang() selector

The :lang() pseudo-class targets elements based on their language. To group rules for related languages, pass multiple language codes to a single :lang().
styles.css
/* Typography adjustments for CJK languages */
:lang(zh, ja, ko) {
  line-height: 1.8;
  font-size: 1.05em;
}

/* Different quote styles by language group */
blockquote:lang(fr, it, es, pt) {
  font-style: italic;
}

blockquote:lang(de, nl, da, sv) {
  font-weight: 500;
}
For browsers that don’t support multiple arguments in the :lang() selector, Bun’s CSS bundler converts this syntax to the :is() selector with the same behavior:
styles.css
/* Multiple languages grouped with :is() for better browser support */
:is(:lang(zh), :lang(ja), :lang(ko)) {
  line-height: 1.8;
  font-size: 1.05em;
}

blockquote:is(:lang(fr), :lang(it), :lang(es), :lang(pt)) {
  font-style: italic;
}

blockquote:is(:lang(de), :lang(nl), :lang(da), :lang(sv)) {
  font-weight: 500;
}
If needed, Bun can generate additional fallbacks for :is() as well.

:is() selector

The :is() pseudo-class function (formerly :matches()) takes a selector list and matches if any selector in the list matches.
styles.css
/* Instead of writing these separately */
/* 
.article h1,
.article h2,
.article h3 {
  margin-top: 1.5em;
}
*/

/* You can write this */
.article :is(h1, h2, h3) {
  margin-top: 1.5em;
}

/* Complex example with multiple groups */
:is(header, main, footer) :is(h1, h2, .title) {
  font-family: "Heading Font", sans-serif;
}
For browsers that don’t support :is(), Bun’s CSS bundler provides fallbacks using vendor-prefixed alternatives:
/* Fallback using -webkit-any */
.article :-webkit-any(h1, h2, h3) {
  margin-top: 1.5em;
}

/* Fallback using -moz-any */
.article :-moz-any(h1, h2, h3) {
  margin-top: 1.5em;
}

/* Original preserved for modern browsers */
.article :is(h1, h2, h3) {
  margin-top: 1.5em;
}

/* Complex example with fallbacks */
:-webkit-any(header, main, footer) :-webkit-any(h1, h2, .title) {
  font-family: "Heading Font", sans-serif;
}

:-moz-any(header, main, footer) :-moz-any(h1, h2, .title) {
  font-family: "Heading Font", sans-serif;
}

:is(header, main, footer) :is(h1, h2, .title) {
  font-family: "Heading Font", sans-serif;
}
The vendor-prefixed versions have limitations compared to the standardized :is() selector, particularly with complex selectors. Bun only uses the prefixed versions when they work correctly.

:not() selector

The :not() pseudo-class excludes elements that match a selector. The modern version accepts multiple arguments to exclude several patterns with one :not().
styles.css
/* Select all buttons except primary and secondary variants */
button:not(.primary, .secondary) {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
}

/* Apply styles to all headings except those inside sidebars or footers */
h2:not(.sidebar *, footer *) {
  margin-top: 2em;
}
For browsers that don’t support multiple arguments in :not(), Bun’s CSS bundler converts this syntax to a more compatible form with the same behavior:
styles.css
/* Converted to use :not with :is() for compatibility */
button:not(:is(.primary, .secondary)) {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
}

h2:not(:is(.sidebar *, footer *)) {
  margin-top: 2em;
}
And if :is() isn’t supported, Bun can generate further fallbacks:
styles.css
/* Even more fallbacks for maximum compatibility */
button:not(:-webkit-any(.primary, .secondary)) {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
}

button:not(:-moz-any(.primary, .secondary)) {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
}

button:not(:is(.primary, .secondary)) {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
}
The converted selectors keep the specificity and behavior of the original.

Math functions

CSS includes standard math functions (round(), mod(), rem(), abs(), sign()), trigonometric functions (sin(), cos(), tan(), asin(), acos(), atan(), atan2()), and exponential functions (pow(), sqrt(), exp(), log(), hypot()).
styles.css
.dynamic-sizing {
  /* Clamp a value between minimum and maximum */
  width: clamp(200px, 50%, 800px);

  /* Round to the nearest multiple */
  padding: round(14.8px, 5px);

  /* Trigonometry for animations or layouts */
  transform: rotate(calc(sin(45deg) * 50deg));

  /* Complex math with multiple functions */
  --scale-factor: pow(1.25, 3);
  font-size: calc(16px * var(--scale-factor));
}
Bun’s CSS bundler evaluates these expressions at build time when all values are known constants (not variables):
styles.css
.dynamic-sizing {
  width: clamp(200px, 50%, 800px);
  padding: 15px;
  transform: rotate(35.36deg);
  --scale-factor: 1.953125;
  font-size: calc(16px * var(--scale-factor));
}

Media query ranges

Media query range syntax expresses breakpoints with comparison operators (<, >, <=, >=) instead of the more verbose min- and max- prefixes.
styles.css
/* Modern syntax with comparison operators */
@media (width >= 768px) {
  .container {
    max-width: 720px;
  }
}

/* Inclusive range using <= and >= */
@media (768px <= width <= 1199px) {
  .sidebar {
    display: flex;
  }
}

/* Exclusive range using < and > */
@media (width > 320px) and (width < 768px) {
  .mobile-only {
    display: block;
  }
}
Bun’s CSS bundler converts range queries to traditional media query syntax for compatibility with all browsers:
styles.css
/* Converted to traditional min/max syntax */
@media (min-width: 768px) {
  .container {
    max-width: 720px;
  }
}

@media (min-width: 768px) and (max-width: 1199px) {
  .sidebar {
    display: flex;
  }
}

@media (min-width: 321px) and (max-width: 767px) {
  .mobile-only {
    display: block;
  }
}

Shorthands

CSS has introduced several shorthand properties that combine multiple longhand properties.
styles.css
/* Alignment shorthands */
.flex-container {
  /* Shorthand for align-items and justify-items */
  place-items: center start;

  /* Shorthand for align-content and justify-content */
  place-content: space-between center;
}

.grid-item {
  /* Shorthand for align-self and justify-self */
  place-self: end center;
}

/* Two-value overflow */
.content-box {
  /* First value for horizontal, second for vertical */
  overflow: hidden auto;
}

/* Enhanced text-decoration */
.fancy-link {
  /* Combines multiple text decoration properties */
  text-decoration: underline dotted blue 2px;
}

/* Two-value display syntax */
.component {
  /* Outer display type + inner display type */
  display: inline flex;
}
For browsers that don’t support these shorthands, Bun converts them to their component longhand properties:
styles.css
.flex-container {
  /* Expanded alignment properties */
  align-items: center;
  justify-items: start;

  align-content: space-between;
  justify-content: center;
}

.grid-item {
  align-self: end;
  justify-self: center;
}

.content-box {
  /* Separate overflow properties */
  overflow-x: hidden;
  overflow-y: auto;
}

.fancy-link {
  /* Individual text decoration properties */
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: blue;
  text-decoration-thickness: 2px;
}

.component {
  /* Single value display */
  display: inline-flex;
}

Double position gradients

Double position gradient syntax specifies the same color at two adjacent positions to create a hard color stop: a sharp transition instead of a smooth fade. Use it for stripes, color bands, and other multi-color designs.
styles.css
.striped-background {
  /* Creates a sharp transition from green to red at 30%-40% */
  background: linear-gradient(
    to right,
    yellow 0%,
    green 20%,
    green 30%,
    red 30%,
    /* Double position creates hard stop */ red 70%,
    blue 70%,
    blue 100%
  );
}

.progress-bar {
  /* Creates distinct color sections */
  background: linear-gradient(
    to right,
    #4caf50 0% 25%,
    /* Green from 0% to 25% */ #ffc107 25% 50%,
    /* Yellow from 25% to 50% */ #2196f3 50% 75%,
    /* Blue from 50% to 75% */ #9c27b0 75% 100% /* Purple from 75% to 100% */
  );
}
For browsers that don’t support this syntax, Bun’s CSS bundler converts it to the traditional format by duplicating color stops:
styles.css
.striped-background {
  background: linear-gradient(
    to right,
    yellow 0%,
    green 20%,
    green 30%,
    red 30%,
    /* Split into two color stops */ red 70%,
    blue 70%,
    blue 100%
  );
}

.progress-bar {
  background: linear-gradient(
    to right,
    #4caf50 0%,
    #4caf50 25%,
    /* Two stops for green section */ #ffc107 25%,
    #ffc107 50%,
    /* Two stops for yellow section */ #2196f3 50%,
    #2196f3 75%,
    /* Two stops for blue section */ #9c27b0 75%,
    #9c27b0 100% /* Two stops for purple section */
  );
}

system-ui font

The system-ui generic font family uses the device’s native UI font.
styles.css
.native-interface {
  /* Use the system's default UI font */
  font-family: system-ui;
}

.fallback-aware {
  /* System UI font with explicit fallbacks */
  font-family: system-ui, sans-serif;
}
For browsers that don’t support system-ui, Bun’s CSS bundler expands it to a cross-platform font stack:
styles.css
.native-interface {
  /* Expanded to support all major platforms */
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    "Noto Sans",
    Ubuntu,
    Cantarell,
    "Helvetica Neue";
}

.fallback-aware {
  /* Preserves the original fallback after the expanded stack */
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    "Noto Sans",
    Ubuntu,
    Cantarell,
    "Helvetica Neue",
    sans-serif;
}
The expanded stack includes system fonts for macOS/iOS, Windows, Android, and Linux, plus fallbacks for older browsers.

CSS Modules

Bun’s bundler also supports CSS modules, with the following features:
  • Detecting CSS module files (.module.css) with no configuration
  • Composition (composes property)
  • Importing CSS modules into JSX/TSX
  • Warnings/errors for invalid usages of CSS modules
A CSS module is a CSS file (with the .module.css extension) where all class names and animations are scoped to the file. This helps you avoid class name collisions, as CSS declarations are globally scoped by default. Bun’s bundler transforms locally scoped class names into unique identifiers.

Getting started

Create a CSS file with the .module.css extension:
styles.module.css
.button {
  color: red;
}
other-styles.module.css
.button {
  color: blue;
}
You can then import this file, for example into a TSX file:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bapp.tsx
import styles from "./styles.module.css";
import otherStyles from "./other-styles.module.css";

export default function App() {
  return (
    <>
      <button className={styles.button}>Red button!</button>
      <button className={otherStyles.button}>Blue button!</button>
    </>
  );
}
Importing a CSS module gives you an object that maps each class name to its unique identifier:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bapp.tsx
import styles from "./styles.module.css";
import otherStyles from "./other-styles.module.css";

console.log(styles);
console.log(otherStyles);
This outputs:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bapp.tsx
{
  button: "button_123";
}

{
  button: "button_456";
}
The class names are unique to each file, so they don’t collide.

Composition

CSS modules can compose class selectors together to reuse style rules across multiple classes. For example:
styles.module.css
.button {
  composes: background;
  color: red;
}

.background {
  background-color: blue;
}
This is the same as writing:
styles.module.css
.button {
  background-color: blue;
  color: red;
}

.background {
  background-color: blue;
}
Two rules apply when using composes:
Composition Rules: - A composes property must come before any regular CSS properties or declarations - You can only use composes on a simple selector with a single class name
styles.module.css
#button {
  /* Invalid! `#button` is not a class selector */
  composes: background;
}

.button,
.button-secondary {
  /* Invalid! `.button, .button-secondary` is not a simple selector */
  composes: background;
}

Composing from a separate CSS module file

You can also compose from a separate CSS module file:
background.module.css
.background {
  background-color: blue;
}
styles.module.css
.button {
  composes: background from "./background.module.css";
  color: red;
}
When composing classes from separate files, make sure they do not contain the same properties.The CSS module spec says that composing classes from separate files with conflicting properties is undefined behavior: the output may differ and be unreliable.