Skip to content

feat(formatter): add TailwindCSS support for JS/TS files#16990

Merged
graphite-app[bot] merged 1 commit into
mainfrom
12-17-feat_formatter_add_tailwind_css_class_sorting_support
Jan 5, 2026
Merged

feat(formatter): add TailwindCSS support for JS/TS files#16990
graphite-app[bot] merged 1 commit into
mainfrom
12-17-feat_formatter_add_tailwind_css_class_sorting_support

Conversation

@Dunqing
Copy link
Copy Markdown
Member

@Dunqing Dunqing commented Dec 17, 2025

Based on #16826

Summary

Add experimental Tailwind CSS class sorting support to Oxfmt via integration with prettier-plugin-tailwindcss.

This implementation uses a patch-based approach to export necessary functions from prettier-plugin-tailwindcss. We've requested better integration support from the Tailwind team at tailwindlabs/prettier-plugin-tailwindcss#430, and can improve the integration when that's available.

Features

  • ✅ Sorts Tailwind CSS classes in className and class attributes
  • ✅ Sorts classes in custom JSX attributes (configurable)
  • ✅ Sorts classes in function calls like clsx(), cn(), tw() (configurable)
  • ✅ Supports template literals with expressions
  • ✅ Supports Tailwind v3 config files and v4 stylesheets
  • ✅ Preserves whitespace and duplicates (configurable)
  • Fixes prettier-plugin-tailwindcss#426 - Correctly handles strings in nested non-Tailwind call expressions
  • ⏳ Non-JS file support (HTML, Vue, Svelte, etc.) - Coming in follow-up PR
  • ⏳ Tailwind CSS config migration in oxfmt --migrate=prettier - Coming in follow-up PR

Example Usage

Basic Usage

Enable Tailwind CSS sorting with default options by setting experimentalTailwindcss to an empty object in your .oxfmtrc.json:

{
  "experimentalTailwindcss": {}
}

With Configuration Options

{
  "experimentalTailwindcss": {
    "config": "./tailwind.config.js",
    "functions": ["clsx", "cn", "cva", "tw"],
    "attributes": ["myCustomClass"],
    "preserveWhitespace": false,
    "preserveDuplicates": false
  }
}

Configuration Options

All options from prettier-plugin-tailwindcss are supported (with tailwind prefix removed):

Option Type Description Default
config string Path to your Tailwind CSS v3 configuration file. Paths are resolved relative to the Oxfmt configuration file. "./tailwind.config.js"
stylesheet string Path to your Tailwind CSS v4 stylesheet. Paths are resolved relative to the Oxfmt configuration file. undefined
functions string[] List of custom function names that contain Tailwind CSS classes (e.g., ["clsx", "cn", "cva", "tw"]). []
attributes string[] List of custom JSX attributes that should be sorted (e.g., ["myCustomClass"]). Note: class and className are always sorted by default. []
preserveWhitespace boolean Preserve whitespace around classes. false
preserveDuplicates boolean Preserve duplicate classes. false

Migration from prettier-plugin-tailwindcss

If you're currently using prettier-plugin-tailwindcss, you can migrate to Oxfmt's built-in support:

Manual Migration (Current)

Before (Prettier)

{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.js",
  "tailwindFunctions": ["clsx", "cn"]
}

After (Oxfmt)

{
  "experimentalTailwindcss": {
    "config": "./tailwind.config.js",
    "functions": ["clsx", "cn"]
  }
}

Key differences:

  • Remove tailwind prefix from all option names (e.g., tailwindConfigconfig)
  • No need to install prettier-plugin-tailwindcss separately, it is bundled within Oxfmt

Automated Migration (Follow-up PR)

Tailwind CSS configuration migration will be added to the existing oxfmt --migrate=prettier command in a follow-up PR. This will automatically convert your prettier-plugin-tailwindcss settings from .prettierrc to .oxfmtrc.json.

Implementation Details

Technical Approach

  • Uses pnpm patch to export necessary functions from prettier-plugin-tailwindcss
  • Implements Rust-side detection for Tailwind contexts (JSX attributes, function calls, template literals)
  • Passes class strings to the JS plugin for sorting via external callback
  • Optimized context tracking to avoid repeated AST traversal

Improvements Over prettier-plugin-tailwindcss

  • Context-based quasi position tracking (tailwindcss.rs:273-276) - Stores template literal position in context stack instead of traversing AST, improving performance
  • Fixes issue #426 - Correctly handles strings in nested non-Tailwind call expressions (e.g., value.includes("\n") no longer gets corrupted)
  • Comprehensive edge case testing - Added 58 tests covering edge cases, nested scenarios, error handling, and performance

Changes

  • Changed experimentalTailwindcss option from boolean to TailwindcssOptions object
  • Added all configuration options from prettier-plugin-tailwindcss (without tailwind prefix)
  • Exported necessary functions from prettier-plugin-tailwindcss via patch
  • Added comprehensive test suite with 58 tests
  • Improved documentation for all configuration options

Test Plan

  • pnpm test passes (58 tests)
  • cargo clippy passes
  • just fmt passes
  • Basic Tailwind class sorting works with class and className attributes
  • attributes option sorts custom JSX attributes
  • functions option sorts string arguments in function calls (e.g., clsx(), cn())
  • Template literals with expressions handle whitespace correctly
  • Edge cases: empty strings, Unicode, emoji, special characters
  • Nested scenarios: deeply nested template literals, function calls
  • Strings in nested non-Tailwind calls are preserved (fixes #426)
  • Test in https://github.com/tailwindlabs/headlessui and https://github.com/dubinc/dub

Follow-up Work

🤖 Generated with Claude Code

@github-actions github-actions Bot added A-cli Area - CLI A-formatter Area - Formatter labels Dec 17, 2025
Copy link
Copy Markdown
Member Author

Dunqing commented Dec 17, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the C-enhancement Category - New feature or request label Dec 17, 2025
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Dec 17, 2025

CodSpeed Performance Report

Merging #16990 will not alter performance

Comparing 12-17-feat_formatter_add_tailwind_css_class_sorting_support (283c61a) with main (f60a4d8)

Summary

✅ 38 untouched
⏩ 7 skipped1

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@leaysgur
Copy link
Copy Markdown
Member

Not quite a review, but here are some notes on what I noticed while looking through... 🚶🏻

  • Even if this change enables sorting for JS/TS, e.g. inside Vue's <template><div class="here"> still won't be sorted
    • Users need to install Prettier and the Prettier plugin separately
    • Concerned about potential version discrepancies here
    • Even if we make it a peerDeps, since this Prettier plugin is bundled, we can't use what we want anyway...
  • However, since getTailwindConfig() internally looks for prettierrc, I'm not sure if it will work correctly for us
  • As we did with sort-imports, there might be some findings from eslint-plugin-better-tailwindcss, instead of the prettier plugin
    • Since this isn't bundled, we might be able to borrow code without patching?
    • Either way, jiti and postcss are still required as deps...
  • We should take sufficient time for testing before merging?
    • Once merged, it seems like it would be difficult to remove later

@Dunqing Dunqing force-pushed the 12-17-feat_formatter_add_tailwind_css_class_sorting_support branch from 1e49e6c to eee8f18 Compare December 18, 2025 11:56
@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Dec 19, 2025

  • Even if this change enables sorting for JS/TS, e.g. inside Vue's <template><div class="here"> still won't be sorted

    • Users need to install Prettier and the Prettier plugin separately
    • Concerned about potential version discrepancies here
    • Even if we make it a peerDeps, since this Prettier plugin is bundled, we can't use what we want anyway...

I don't think this is a problem. We can likely load the original prettier-plugin-tailwindcss plugin while formatting non-JS files, which delegates to Prettier.

I've noticed the same thing; the absence of Prettier in the codebases won't have any impact. However, this situation is not ideal, as there is no reason to look up Prettier here. Additionally, the current patching approach is for testing the whole thing. When the PR is ready, we need to request changes upstream since we are patching the minified source. But thank you, I will take a look at the eslint-plugin-better-tailwindcss

  • We should take sufficient time for testing before merging?

    • Once merged, it seems like it would be difficult to remove later

Yes, I started to test in real codebases

@danciudev
Copy link
Copy Markdown

I always found the official plugin order too lax and inconsistent; personally, I prefer https://github.com/schoero/eslint-plugin-better-tailwindcss

@Dunqing Dunqing force-pushed the 12-17-feat_formatter_add_tailwind_css_class_sorting_support branch 2 times, most recently from b0d82c9 to 7fd5842 Compare December 30, 2025 01:27
@Dunqing Dunqing force-pushed the 12-17-feat_formatter_add_tailwind_css_class_sorting_support branch 3 times, most recently from 0d4ab65 to c6b7cc1 Compare January 5, 2026 02:26
@Dunqing Dunqing changed the title feat(formatter): add Tailwind CSS class sorting support feat(formatter): add TailwindCSS support for JS/TS files Jan 5, 2026
@Dunqing Dunqing marked this pull request as ready for review January 5, 2026 05:19
Copilot AI review requested due to automatic review settings January 5, 2026 05:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds experimental Tailwind CSS class sorting support to Oxfmt by integrating with prettier-plugin-tailwindcss. The implementation uses a patch-based approach to export necessary functions from the plugin while awaiting official integration support.

Key changes:

  • Rust-side detection for Tailwind contexts (JSX attributes, function calls, template literals) with optimized context tracking
  • External callback system for JS-side class sorting via prettier-plugin-tailwindcss
  • Comprehensive configuration options matching prettier-plugin-tailwindcss (with tailwind prefix removed)

Reviewed changes

Copilot reviewed 36 out of 39 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/oxc_formatter/src/utils/tailwindcss.rs Core Tailwind detection and write utilities for JSX attributes, functions, and template literals
crates/oxc_formatter/src/formatter/context.rs TailwindContextEntry stack for tracking Tailwind contexts during traversal
crates/oxc_formatter/src/external_formatter.rs Renamed from embedded_formatter, now handles both embedded formatting and Tailwind sorting
crates/oxc_formatter/src/formatter/format_element/mod.rs New FormatElement::TailwindClass variant for deferred class sorting
crates/oxc_formatter/src/formatter/printer/mod.rs Printer updated to resolve TailwindClass indices to sorted strings
crates/oxc_formatter/src/write/* JSXAttribute, CallExpression, StringLiteral, and TemplateElement formatting with Tailwind context awareness
apps/oxfmt/src-js/libs/prettier.ts sortTailwindClasses function wrapping prettier-plugin-tailwindcss
apps/oxfmt/test/tailwindcss.test.ts Comprehensive test suite with 58 tests covering edge cases
patches/prettier-plugin-tailwindcss@0.7.2.patch Patch to export getTailwindConfig and sortClasses functions
crates/oxc_formatter/src/oxfmtrc.rs Configuration schema for TailwindcssConfig
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/oxc_formatter/src/utils/tailwindcss.rs Outdated
@Dunqing Dunqing requested a review from leaysgur January 5, 2026 05:32
Comment thread apps/oxfmt/src-js/cli/worker-proxy.ts Outdated
Comment thread apps/oxfmt/src-js/prettier-plugin-tailwindcss.d.ts Outdated
Comment thread apps/oxfmt/package.json Outdated
Comment thread apps/oxfmt/tsdown.config.ts Outdated
Comment thread crates/oxc_formatter/src/oxfmtrc.rs
@leaysgur
Copy link
Copy Markdown
Member

leaysgur commented Jan 5, 2026

Oh, CI is failing. 😮

@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Jan 5, 2026

Oh, CI is failing. 😮

Not sure why the oxfmt tests took too long; I've fixed it by increasing the timeout duration.

@Dunqing Dunqing added the 0-merge Merge with Graphite Merge Queue label Jan 5, 2026
Copy link
Copy Markdown
Member Author

Dunqing commented Jan 5, 2026

Merge activity

Based on #16826

## Summary

Add experimental Tailwind CSS class sorting support to Oxfmt via integration with `prettier-plugin-tailwindcss`.

This implementation uses a patch-based approach to export necessary functions from `prettier-plugin-tailwindcss`. We've requested better integration support from the Tailwind team at tailwindlabs/prettier-plugin-tailwindcss#430, and can improve the integration when that's available.

## Features

- ✅ Sorts Tailwind CSS classes in `className` and `class` attributes
- ✅ Sorts classes in custom JSX attributes (configurable)
- ✅ Sorts classes in function calls like `clsx()`, `cn()`, `tw()` (configurable)
- ✅ Supports template literals with expressions
- ✅ Supports Tailwind v3 config files and v4 stylesheets
- ✅ Preserves whitespace and duplicates (configurable)
- ✅ **Fixes [prettier-plugin-tailwindcss#426](tailwindlabs/prettier-plugin-tailwindcss#426 - Correctly handles strings in nested non-Tailwind call expressions
- ⏳ Non-JS file support (HTML, Vue, Svelte, etc.) - Coming in follow-up PR
- ⏳ Tailwind CSS config migration in `oxfmt --migrate=prettier` - Coming in follow-up PR

## Example Usage

### Basic Usage

Enable Tailwind CSS sorting with default options by setting `experimentalTailwindcss` to an empty object in your `.oxfmtrc.json`:

```json
{
  "experimentalTailwindcss": {}
}
```

### With Configuration Options

```json
{
  "experimentalTailwindcss": {
    "config": "./tailwind.config.js",
    "functions": ["clsx", "cn", "cva", "tw"],
    "attributes": ["myCustomClass"],
    "preserveWhitespace": false,
    "preserveDuplicates": false
  }
}
```

### Configuration Options

All options from `prettier-plugin-tailwindcss` are supported (with `tailwind` prefix removed):

| Option | Type | Description | Default |
|--------|------|-------------|---------|
| `config` | `string` | Path to your Tailwind CSS v3 configuration file. Paths are resolved relative to the Oxfmt configuration file. | `"./tailwind.config.js"` |
| `stylesheet` | `string` | Path to your Tailwind CSS v4 stylesheet. Paths are resolved relative to the Oxfmt configuration file. | `undefined` |
| `functions` | `string[]` | List of custom function names that contain Tailwind CSS classes (e.g., `["clsx", "cn", "cva", "tw"]`). | `[]` |
| `attributes` | `string[]` | List of custom JSX attributes that should be sorted (e.g., `["myCustomClass"]`). Note: `class` and `className` are always sorted by default. | `[]` |
| `preserveWhitespace` | `boolean` | Preserve whitespace around classes. | `false` |
| `preserveDuplicates` | `boolean` | Preserve duplicate classes. | `false` |

## Migration from prettier-plugin-tailwindcss

If you're currently using `prettier-plugin-tailwindcss`, you can migrate to Oxfmt's built-in support:

### Manual Migration (Current)

#### Before (Prettier)

```json
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.js",
  "tailwindFunctions": ["clsx", "cn"]
}
```

#### After (Oxfmt)

```json
{
  "experimentalTailwindcss": {
    "config": "./tailwind.config.js",
    "functions": ["clsx", "cn"]
  }
}
```

**Key differences:**
- Remove `tailwind` prefix from all option names (e.g., `tailwindConfig` → `config`)
- No need to install `prettier-plugin-tailwindcss` separately, it is bundled within Oxfmt
### Automated Migration (Follow-up PR)

Tailwind CSS configuration migration will be added to the existing `oxfmt --migrate=prettier` command in a follow-up PR. This will automatically convert your `prettier-plugin-tailwindcss` settings from `.prettierrc` to `.oxfmtrc.json`.

## Implementation Details

### Technical Approach

- Uses `pnpm patch` to export necessary functions from `prettier-plugin-tailwindcss`
- Implements Rust-side detection for Tailwind contexts (JSX attributes, function calls, template literals)
- Passes class strings to the JS plugin for sorting via external callback
- Optimized context tracking to avoid repeated AST traversal

### Improvements Over prettier-plugin-tailwindcss

- **Context-based quasi position tracking** ([tailwindcss.rs:273-276](https://github.com/oxc-project/oxc/blob/main/crates/oxc_formatter/src/utils/tailwindcss.rs#L273-L276)) - Stores template literal position in context stack instead of traversing AST, improving performance
- **Fixes [issue #426](tailwindlabs/prettier-plugin-tailwindcss#426 - Correctly handles strings in nested non-Tailwind call expressions (e.g., `value.includes("\n")` no longer gets corrupted)
- **Comprehensive edge case testing** - Added 58 tests covering edge cases, nested scenarios, error handling, and performance

## Changes

- Changed `experimentalTailwindcss` option from `boolean` to `TailwindcssOptions` object
- Added all configuration options from `prettier-plugin-tailwindcss` (without `tailwind` prefix)
- Exported necessary functions from `prettier-plugin-tailwindcss` via patch
- Added comprehensive test suite with 58 tests
- Improved documentation for all configuration options

## Test Plan

- [x] `pnpm test` passes (58 tests)
- [x] `cargo clippy` passes
- [x] `just fmt` passes
- [x] Basic Tailwind class sorting works with `class` and `className` attributes
- [x] `attributes` option sorts custom JSX attributes
- [x] `functions` option sorts string arguments in function calls (e.g., `clsx()`, `cn()`)
- [x] Template literals with expressions handle whitespace correctly
- [x] Edge cases: empty strings, Unicode, emoji, special characters
- [x] Nested scenarios: deeply nested template literals, function calls
- [x] Strings in nested non-Tailwind calls are preserved (fixes [#426](tailwindlabs/prettier-plugin-tailwindcss#426))
- [x] Test in https://github.com/tailwindlabs/headlessui and https://github.com/dubinc/dub

## Follow-up Work

- **Non-JS file support** - HTML, Vue, Svelte, etc. by enabling original plugin when `experimentalTailwindcss` is set
- **Migration support** - Add Tailwind CSS config migration to existing `oxfmt --migrate=prettier` command
- **Better integration** - Improve when tailwindlabs/prettier-plugin-tailwindcss#430 is resolved

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@graphite-app graphite-app Bot force-pushed the 12-17-feat_formatter_add_tailwind_css_class_sorting_support branch from 283c61a to 26ed46b Compare January 5, 2026 10:07
@graphite-app graphite-app Bot merged commit 26ed46b into main Jan 5, 2026
21 checks passed
@graphite-app graphite-app Bot deleted the 12-17-feat_formatter_add_tailwind_css_class_sorting_support branch January 5, 2026 10:13
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jan 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-formatter Area - Formatter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Removes newline character in .includes() inside classNames

4 participants