Skip to content

CSS modules #17958

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
884 changes: 884 additions & 0 deletions docs/bundler/css.md

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions docs/bundler/css_modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# CSS Modules

Bun's bundler also supports bundling [CSS modules](https://css-tricks.com/css-modules-part-1-need/) in addition to [regular CSS](/docs/bundler/css) with support for the following features:

- Automatically detecting CSS module files (`.module.css`) with zero 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 are 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.

Under the hood, Bun's bundler transforms locally scoped class names into unique identifiers.

## Getting started

Create a CSS file with the `.module.css` extension:

```css
/* 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:

```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>
</>
);
}
```

The `styles` object from importing the CSS module file will be an object with all class names as keys and
their unique identifiers as values:

```tsx
import styles from "./styles.module.css";
import otherStyles from "./other-styles.module.css";

console.log(styles);
console.log(otherStyles);
```

This will output:

```ts
{
button: "button_123";
}

{
button: "button_456";
}
```

As you can see, the class names are unique to each file, avoiding any collisions!

### Composition

CSS modules allow you to _compose_ class selectors together. This lets you reuse style rules across multiple classes.

For example:

```css
/* styles.module.css */
.button {
composes: background;
color: red;
}

.background {
background-color: blue;
}
```

Would be the same as writing:

```css
.button {
background-color: blue;
color: red;
}

.background {
background-color: blue;
}
```

{% callout %}
There are a couple rules to keep in mind when using `composes`:

- 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**:

```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;
}
```

{% /callout %}

### Composing from a separate CSS module file

You can also compose from a separate CSS module file:

```css
/* background.module.css */
.background {
background-color: blue;
}

/* styles.module.css */
.button {
composes: background from "./background.module.css";
color: red;
}
```

{% callout %}
When composing classes from separate files, be sure that they do not contain the same properties.

The CSS module spec says that composing classes from separate files with conflicting properties is
undefined behavior, meaning that the output may differ and be unreliable.
{% /callout %}
6 changes: 6 additions & 0 deletions src/baby_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ pub fn BabyList(comptime Type: type) type {
this.update(list__);
}

pub fn insertSlice(this: *@This(), allocator: std.mem.Allocator, index: usize, vals: []const Type) !void {
var list__ = this.listManaged(allocator);
try list__.insertSlice(index, vals);
this.update(list__);
}

pub fn append(this: *@This(), allocator: std.mem.Allocator, value: []const Type) !void {
var list__ = this.listManaged(allocator);
try list__.appendSlice(value);
Expand Down
11 changes: 10 additions & 1 deletion src/bake/DevServer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,15 @@ fn appendRouteEntryPointsIfNotStale(dev: *DevServer, entry_points: *EntryPointLi
try entry_points.append(alloc, html.html_bundle.html_bundle.path, .{ .client = true });
},
}

if (dev.has_tailwind_plugin_hack) |*map| {
for (map.keys()) |abs_path| {
const file = dev.client_graph.bundled_files.get(abs_path) orelse
continue;
if (file.flags.kind == .css)
entry_points.appendCss(alloc, abs_path) catch bun.outOfMemory();
}
}
}

fn onFrameworkRequestWithBundle(
Expand Down Expand Up @@ -3583,7 +3592,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
bundler_index: bun.JSAst.Index,
) !void {
bun.assert(bundler_index.isValid());
bun.assert(ctx.loaders[bundler_index.get()] == .css);
bun.assert(ctx.loaders[bundler_index.get()].isCSS());

var sfb = std.heap.stackFallback(@sizeOf(bun.JSAst.Index) * 64, temp_alloc);
const queue_alloc = sfb.get();
Expand Down
4 changes: 2 additions & 2 deletions src/bake/production.zig
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa
file.dest_path,
file.entry_point_index,
});
if (file.loader == .css) {
if (file.loader.isCSS()) {
if (css_chunks_count == 0) css_chunks_first = i;
css_chunks_count += 1;
}
Expand Down Expand Up @@ -417,7 +417,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa
const css_chunk_js_strings = try allocator.alloc(JSValue, css_chunks_count);
for (bundled_outputs[css_chunks_first..][0..css_chunks_count], css_chunk_js_strings) |output_file, *str| {
bun.assert(output_file.dest_path[0] != '.');
bun.assert(output_file.loader == .css);
bun.assert(output_file.loader.isCSS());
str.* = (try bun.String.createFormat("{s}{s}", .{ public_path, output_file.dest_path })).toJS(global);
}

Expand Down
14 changes: 14 additions & 0 deletions src/base64/base64.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ pub fn encode(destination: []u8, source: []const u8) usize {
return bun.simdutf.base64.encode(source, destination, false);
}

pub fn simdutfEncodeLenUrlSafe(source_len: usize) usize {
return bun.simdutf.base64.encode_len(source_len, true);
}

/// Encode with the following differences from regular `encode` function:
///
/// * No padding is added (the extra `=` characters at the end)
/// * `-` and `_` are used instead of `+` and `/`
///
/// See the documentation for simdutf's `binary_to_base64` function for more details (simdutf_impl.h).
pub fn simdutfEncodeUrlSafe(destination: []u8, source: []const u8) usize {
return bun.simdutf.base64.encode(source, destination, true);
}

pub fn decodeLenUpperBound(len: usize) usize {
return zig_base64.standard.Decoder.calcSizeUpperBound(len) catch {
//fallback
Expand Down
10 changes: 9 additions & 1 deletion src/bun.js/api/BunObject.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3622,7 +3622,15 @@ const TOMLObject = struct {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
var writer = js_printer.BufferPrinter.init(buffer_writer);
_ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch {
_ = js_printer.printJSON(
*js_printer.BufferPrinter,
&writer,
parse_result,
&source,
.{
.mangled_props = null,
},
) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};

Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/api/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7464,7 +7464,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
&writer,
bun.Global.BunInfo.generate(*Transpiler, &JSC.VirtualMachine.get().transpiler, allocator) catch unreachable,
&source,
.{},
.{ .mangled_props = null },
) catch unreachable;

resp.writeStatus("200 OK");
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/bindings/bun-simdutf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ size_t simdutf__base64_encode(const char* input, size_t length, char* output, in
return simdutf::binary_to_base64(input, length, output, is_urlsafe ? simdutf::base64_url : simdutf::base64_default);
}

size_t simdutf__base64_length_from_binary(size_t length, int is_urlsafe)
{
return simdutf::base64_length_from_binary(length, is_urlsafe ? simdutf::base64_url : simdutf::base64_default);
}

SIMDUTFResult simdutf__base64_decode_from_binary(const char* input, size_t length, char* output, size_t outlen_, int is_urlsafe)
{
size_t outlen = outlen_;
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/bindings/bun-simdutf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,16 @@ pub const base64 = struct {
extern fn simdutf__base64_encode(input: [*]const u8, length: usize, output: [*]u8, is_urlsafe: c_int) usize;
extern fn simdutf__base64_decode_from_binary(input: [*]const u8, length: usize, output: [*]u8, outlen: usize, is_urlsafe: c_int) SIMDUTFResult;
extern fn simdutf__base64_decode_from_binary16(input: [*]const u16, length: usize, output: [*]u8, outlen: usize, is_urlsafe: c_int) SIMDUTFResult;
extern fn simdutf__base64_length_from_binary(length: usize, options: c_int) usize;

pub fn encode(input: []const u8, output: []u8, is_urlsafe: bool) usize {
return simdutf__base64_encode(input.ptr, input.len, output.ptr, @intFromBool(is_urlsafe));
}

pub fn encode_len(input: usize, is_urlsafe: bool) usize {
return simdutf__base64_length_from_binary(input, @intFromBool(is_urlsafe));
}

pub fn decode(input: []const u8, output: []u8, is_urlsafe: bool) SIMDUTFResult {
return simdutf__base64_decode_from_binary(input.ptr, input.len, output.ptr, output.len, @intFromBool(is_urlsafe));
}
Expand Down
Loading