From d8782ed189413e3f850925c7cb8a3b4e7f7b9039 Mon Sep 17 00:00:00 2001
From: Evan Simpson <25159851+e-simpson@users.noreply.github.com>
Date: Mon, 27 Jul 2026 21:30:25 -0400
Subject: [PATCH 1/4] feat: add brightness filter utilities
---
docs/astro.config.mjs | 1 +
docs/src/content/docs/reference/filters.md | 36 +++++++++++++++
src/babel/plugin/visitors/className.test.ts | 15 ++++++
src/index.ts | 1 +
src/parser/filters.test.ts | 47 +++++++++++++++++++
src/parser/filters.ts | 51 +++++++++++++++++++++
src/parser/index.ts | 3 ++
src/types/core.ts | 4 +-
src/utils/mergeStyles.ts | 7 +--
9 files changed, 161 insertions(+), 4 deletions(-)
create mode 100644 docs/src/content/docs/reference/filters.md
create mode 100644 src/parser/filters.test.ts
create mode 100644 src/parser/filters.ts
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index d6e7a0b..7484856 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -57,6 +57,7 @@ export default defineConfig({
{ label: "Borders", slug: "reference/borders" },
{ label: "Outlines", slug: "reference/outlines" },
{ label: "Shadows & Elevation", slug: "reference/shadows" },
+ { label: "Filters", slug: "reference/filters" },
{ label: "Aspect Ratio", slug: "reference/aspect-ratio" },
{ label: "Transforms", slug: "reference/transforms" },
{ label: "Sizing", slug: "reference/sizing" },
diff --git a/docs/src/content/docs/reference/filters.md b/docs/src/content/docs/reference/filters.md
new file mode 100644
index 0000000..0c90138
--- /dev/null
+++ b/docs/src/content/docs/reference/filters.md
@@ -0,0 +1,36 @@
+---
+title: Filters
+description: Apply native visual filters to views
+---
+
+Apply brightness filters with React Native's `filter` style property.
+
+> **Note**: Filters require a React Native version and renderer that support the `filter` style property. Brightness is available on both iOS and Android when native filter support is enabled.
+
+## Brightness
+
+Numeric utilities use percentages, matching Tailwind CSS:
+
+```tsx
+ // filter: [{ brightness: 0.5 }]
+ // filter: [{ brightness: 1 }]
+ // filter: [{ brightness: 1.25 }]
+ // filter: [{ brightness: 2 }]
+```
+
+Numeric brightness classes are dynamic, so values outside the examples also work:
+
+```tsx
+ // filter: [{ brightness: 1.01 }]
+```
+
+## Arbitrary Values
+
+Use bracket syntax for raw React Native amounts or percentages:
+
+```tsx
+ // filter: [{ brightness: 1.01 }]
+ // filter: [{ brightness: 0.8 }]
+```
+
+Brightness values must be non-negative. Unsupported units and negative values are ignored with a development warning.
diff --git a/src/babel/plugin/visitors/className.test.ts b/src/babel/plugin/visitors/className.test.ts
index 0f9a484..4e9c67c 100644
--- a/src/babel/plugin/visitors/className.test.ts
+++ b/src/babel/plugin/visitors/className.test.ts
@@ -23,6 +23,21 @@ describe("className visitor - basic transformation", () => {
expect(output).toContain("style:");
});
+ it("should transform brightness filters", () => {
+ const input = `
+ import { View } from 'react-native';
+ export function Component() {
+ return ;
+ }
+ `;
+
+ const output = transform(input, undefined, true);
+
+ expect(output).not.toContain("className");
+ expect(output).toContain("_brightness_1_01");
+ expect(output).toMatch(/filter:\s*\[\s*{\s*brightness:\s*1\.01/);
+ });
+
it("should work with both tw and className in same file", () => {
const input = `
import { tw } from '@mgcrea/react-native-tailwind';
diff --git a/src/index.ts b/src/index.ts
index 0bd46f6..62f16cb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,6 +24,7 @@ export {
parseAspectRatio,
parseBorder,
parseColor,
+ parseFilter,
parseLayout,
parseOutline,
parsePlaceholderClass,
diff --git a/src/parser/filters.test.ts b/src/parser/filters.test.ts
new file mode 100644
index 0000000..5f25994
--- /dev/null
+++ b/src/parser/filters.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+
+import { parseFilter } from "./filters";
+import { parseClassName } from "./index";
+
+describe("parseFilter - brightness", () => {
+ it("should parse numeric brightness values as percentages", () => {
+ expect(parseFilter("brightness-0")).toEqual({ filter: [{ brightness: 0 }] });
+ expect(parseFilter("brightness-50")).toEqual({ filter: [{ brightness: 0.5 }] });
+ expect(parseFilter("brightness-100")).toEqual({ filter: [{ brightness: 1 }] });
+ expect(parseFilter("brightness-101")).toEqual({ filter: [{ brightness: 1.01 }] });
+ expect(parseFilter("brightness-125")).toEqual({ filter: [{ brightness: 1.25 }] });
+ expect(parseFilter("brightness-200")).toEqual({ filter: [{ brightness: 2 }] });
+ });
+
+ it("should parse arbitrary numeric brightness values", () => {
+ expect(parseFilter("brightness-[1.01]")).toEqual({ filter: [{ brightness: 1.01 }] });
+ expect(parseFilter("brightness-[.5]")).toEqual({ filter: [{ brightness: 0.5 }] });
+ expect(parseFilter("brightness-[2]")).toEqual({ filter: [{ brightness: 2 }] });
+ });
+
+ it("should parse arbitrary percentage brightness values", () => {
+ expect(parseFilter("brightness-[80%]")).toEqual({ filter: [{ brightness: 0.8 }] });
+ expect(parseFilter("brightness-[101%]")).toEqual({ filter: [{ brightness: 1.01 }] });
+ });
+
+ it("should reject unsupported or negative brightness values", () => {
+ expect(parseFilter("brightness-[-1]")).toBeNull();
+ expect(parseFilter("brightness-[1.01px]")).toBeNull();
+ expect(parseFilter("brightness-[abc]")).toBeNull();
+ expect(parseFilter("brightness-auto")).toBeNull();
+ expect(parseFilter("brightness-")).toBeNull();
+ });
+
+ it("should return null for unrelated classes", () => {
+ expect(parseFilter("contrast-100")).toBeNull();
+ expect(parseFilter("bg-white")).toBeNull();
+ expect(parseFilter("")).toBeNull();
+ });
+
+ it("should integrate with the class parser", () => {
+ expect(parseClassName("brightness-[1.01] opacity-80")).toEqual({
+ filter: [{ brightness: 1.01 }],
+ opacity: 0.8,
+ });
+ });
+});
diff --git a/src/parser/filters.ts b/src/parser/filters.ts
new file mode 100644
index 0000000..1f2fdc7
--- /dev/null
+++ b/src/parser/filters.ts
@@ -0,0 +1,51 @@
+/**
+ * Filter utilities supported by React Native's filter style property.
+ */
+
+import type { StyleObject } from "../types";
+
+const NUMBER_PATTERN = String.raw`(?:\d+(?:\.\d*)?|\.\d+)`;
+
+/**
+ * Parse a Tailwind brightness amount.
+ * Named numeric utilities are percentages (brightness-101 -> 1.01), while
+ * bracketed numbers are raw React Native amounts (brightness-[1.01] -> 1.01).
+ */
+function parseBrightnessAmount(value: string): number | null {
+ const arbitraryMatch = value.match(new RegExp(`^\\[(${NUMBER_PATTERN})(%)?\\]$`));
+ if (arbitraryMatch) {
+ const amount = Number.parseFloat(arbitraryMatch[1]);
+ return arbitraryMatch[2] === "%" ? amount / 100 : amount;
+ }
+
+ if (value.startsWith("[") && value.endsWith("]")) {
+ /* v8 ignore next 5 */
+ if (process.env.NODE_ENV !== "production") {
+ console.warn(
+ `[react-native-tailwind] Invalid arbitrary brightness value: ${value}. Only non-negative numbers and percentages are supported (e.g., [1.01], [80%]).`,
+ );
+ }
+ return null;
+ }
+
+ if (new RegExp(`^${NUMBER_PATTERN}$`).test(value)) {
+ return Number.parseFloat(value) / 100;
+ }
+
+ return null;
+}
+
+/**
+ * Parse filter classes.
+ * @param cls - The class name to parse
+ */
+export function parseFilter(cls: string): StyleObject | null {
+ if (cls.startsWith("brightness-")) {
+ const amount = parseBrightnessAmount(cls.substring(11));
+ if (amount !== null) {
+ return { filter: [{ brightness: amount }] };
+ }
+ }
+
+ return null;
+}
diff --git a/src/parser/index.ts b/src/parser/index.ts
index 0e89fcc..1fbf197 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -8,6 +8,7 @@ import { mergeStyles } from "../utils/mergeStyles";
import { parseAspectRatio } from "./aspectRatio";
import { parseBorder } from "./borders";
import { parseColor } from "./colors";
+import { parseFilter } from "./filters";
import { parseLayout } from "./layout";
import { parseOutline } from "./outline";
import { parseShadow } from "./shadows";
@@ -63,6 +64,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
(cls: string) => parseSizing(cls, customTheme?.spacing),
(cls: string) => parseShadow(cls, customTheme?.colors),
+ parseFilter,
parseAspectRatio,
(cls: string) => parseTransform(cls, customTheme?.spacing),
];
@@ -87,6 +89,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
export { parseAspectRatio } from "./aspectRatio";
export { parseBorder } from "./borders";
export { parseColor } from "./colors";
+export { parseFilter } from "./filters";
export { parseLayout } from "./layout";
export { parseOutline } from "./outline";
export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
diff --git a/src/types/core.ts b/src/types/core.ts
index 1ea60c5..06ad056 100644
--- a/src/types/core.ts
+++ b/src/types/core.ts
@@ -18,9 +18,11 @@ export type TransformStyle =
| { perspective?: number };
export type ShadowOffsetStyle = { width: number; height: number };
+export type FilterStyle = { brightness: number };
export type StyleObject = {
- [key: string]: string | number | ShadowOffsetStyle | TransformStyle[] | undefined;
+ [key: string]: string | number | ShadowOffsetStyle | TransformStyle[] | FilterStyle[] | undefined;
+ filter?: FilterStyle[];
shadowOffset?: ShadowOffsetStyle;
transform?: TransformStyle[];
};
diff --git a/src/utils/mergeStyles.ts b/src/utils/mergeStyles.ts
index 043a6a3..79144d3 100644
--- a/src/utils/mergeStyles.ts
+++ b/src/utils/mergeStyles.ts
@@ -84,13 +84,14 @@ export function mergeStyles(target: StyleObject, source: StyleObject): StyleObje
// Handle transform arrays specially
if (key === "transform" && Array.isArray(sourceValue)) {
- const targetValue = target[key];
+ const sourceTransforms = sourceValue as TransformStyle[];
+ const targetValue = target.transform;
if (Array.isArray(targetValue)) {
// Merge transforms with "last wins" for same types
- target.transform = mergeTransforms(targetValue, sourceValue);
+ target.transform = mergeTransforms(targetValue, sourceTransforms);
} else {
// No existing array, just assign
- target[key] = sourceValue;
+ target.transform = sourceTransforms;
}
} else {
// Standard Object.assign behavior for non-transform properties
From 2903d36e429fb62a1418ebe812a85ce793797eee Mon Sep 17 00:00:00 2001
From: Evan Simpson <25159851+e-simpson@users.noreply.github.com>
Date: Mon, 27 Jul 2026 21:39:39 -0400
Subject: [PATCH 2/4] feat: expand native filter support
---
docs/src/content/docs/reference/filters.md | 122 +++++++++++--
src/babel/plugin/visitors/className.test.ts | 17 ++
src/index.ts | 3 +-
src/parser/filters.test.ts | 138 ++++++++++++---
src/parser/filters.ts | 180 +++++++++++++++++++-
src/parser/index.ts | 2 +-
src/types/core.ts | 17 +-
src/utils/mergeStyles.test.ts | 23 +++
src/utils/mergeStyles.ts | 45 ++++-
9 files changed, 492 insertions(+), 55 deletions(-)
diff --git a/docs/src/content/docs/reference/filters.md b/docs/src/content/docs/reference/filters.md
index 0c90138..3ab15e8 100644
--- a/docs/src/content/docs/reference/filters.md
+++ b/docs/src/content/docs/reference/filters.md
@@ -3,34 +3,126 @@ title: Filters
description: Apply native visual filters to views
---
-Apply brightness filters with React Native's `filter` style property.
+Apply composable visual filters with React Native's native `filter` style property.
-> **Note**: Filters require a React Native version and renderer that support the `filter` style property. Brightness is available on both iOS and Android when native filter support is enabled.
+> **Note**: Filters require a React Native version and renderer that support the `filter` style property. Applying a filter also implies `overflow: hidden`, so descendants are clipped to the view's bounds.
-## Brightness
+## Platform Support
-Numeric utilities use percentages, matching Tailwind CSS:
+| Utility | iOS | Android |
+|---------|-----|---------|
+| `brightness-*` | ✅ | ✅ |
+| `blur-*` | ❌ | ✅ |
+| `contrast-*` | ❌ | ✅ |
+| `drop-shadow-*` | ❌ | ✅ |
+| `grayscale-*` | ❌ | ✅ |
+| `hue-rotate-*` | ❌ | ✅ |
+| `invert-*` | ❌ | ✅ |
+| `saturate-*` | ❌ | ✅ |
+| `sepia-*` | ❌ | ✅ |
+
+React Native also supports filter-level opacity on iOS and Android, but Tailwind's `opacity-*` class already maps to the native `opacity` style property in this package. It remains a regular opacity utility to avoid an ambiguous class collision.
+
+## Percentage Filters
+
+Brightness, contrast, grayscale, invert, saturate, and sepia use percentage-based numeric utilities:
+
+```tsx
+ // { brightness: 1.01 }
+ // { contrast: 1.25 }
+ // { grayscale: 0.5 }
+ // { invert: 0.25 }
+ // { saturate: 1.5 }
+ // { sepia: 0.75 }
+```
+
+The full-effect forms are also supported:
+
+```tsx
+
+```
+
+Use bracket syntax for raw React Native amounts or explicit percentages:
+
+```tsx
+ // { brightness: 1.01 }
+ // { contrast: 0.8 }
+```
+
+## Blur
+
+Blur uses Tailwind's pixel scale:
+
+```tsx
+ // { blur: 4 }
+ // { blur: 8 }
+ // { blur: 12 }
+ // { blur: 16 }
+ // { blur: 24 }
+ // { blur: 40 }
+ // { blur: 64 }
+
+```
+
+Arbitrary blur values accept non-negative pixels:
```tsx
- // filter: [{ brightness: 0.5 }]
- // filter: [{ brightness: 1 }]
- // filter: [{ brightness: 1.25 }]
- // filter: [{ brightness: 2 }]
+
+
```
-Numeric brightness classes are dynamic, so values outside the examples also work:
+## Hue Rotation
+
+Numeric hue rotation utilities use degrees and support negative values:
+
+```tsx
+ // { hueRotate: '45deg' }
+ // { hueRotate: '-90deg' }
+```
+
+Arbitrary values support `deg` and `rad` angles:
+
+```tsx
+
+
+```
+
+## Drop Shadow
+
+Drop shadows use Tailwind's `xs` through `2xl` presets and operate on the rendered alpha mask:
```tsx
- // filter: [{ brightness: 1.01 }]
+
+
+
+
```
-## Arbitrary Values
+Arbitrary drop shadows accept X offset, Y offset, optional blur, and a preset, custom, or hex color:
+
+```tsx
+
+
+```
+
+Standalone `drop-shadow-{color}` utilities are not emitted because React Native requires a complete `dropShadow` object rather than Tailwind's separate CSS color variable. Put the color in an arbitrary drop shadow instead.
+
+## Combining Filters
+
+Different filter utilities compile into one ordered native filter array. If the same filter type appears more than once, the last value wins:
+
+```tsx
+
+// filter: [{ blur: 8 }, { brightness: 1.1 }, { contrast: 1.25 }]
+
+
+// filter: [{ brightness: 0.9 }]
+```
-Use bracket syntax for raw React Native amounts or percentages:
+Use `filter-none` to clear filters that appear before it:
```tsx
- // filter: [{ brightness: 1.01 }]
- // filter: [{ brightness: 0.8 }]
+ // filter: []
```
-Brightness values must be non-negative. Unsupported units and negative values are ignored with a development warning.
+Unsupported units, malformed drop shadows, and negative values where React Native requires non-negative amounts are ignored with a development warning.
diff --git a/src/babel/plugin/visitors/className.test.ts b/src/babel/plugin/visitors/className.test.ts
index 4e9c67c..d297b9a 100644
--- a/src/babel/plugin/visitors/className.test.ts
+++ b/src/babel/plugin/visitors/className.test.ts
@@ -38,6 +38,23 @@ describe("className visitor - basic transformation", () => {
expect(output).toMatch(/filter:\s*\[\s*{\s*brightness:\s*1\.01/);
});
+ it("should transform composed and structured filters", () => {
+ const input = `
+ import { View } from 'react-native';
+ export function Component() {
+ return ;
+ }
+ `;
+
+ const output = transform(input, undefined, true);
+
+ expect(output).not.toContain("className");
+ expect(output).toMatch(/blur:\s*8/);
+ expect(output).toMatch(/hueRotate:\s*["']45deg["']/);
+ expect(output).toMatch(/dropShadow:\s*{\s*offsetX:\s*0,\s*offsetY:\s*4/);
+ expect(output).toContain('color: "#00000080"');
+ });
+
it("should work with both tw and className in same file", () => {
const input = `
import { tw } from '@mgcrea/react-native-tailwind';
diff --git a/src/index.ts b/src/index.ts
index 62f16cb..d533065 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,7 +13,7 @@ export { mergeStyles } from "./utils/mergeStyles";
export { generateStyleKey } from "./utils/styleKey";
// Re-export types
-export type { StyleObject } from "./types/core";
+export type { DropShadowStyle, FilterStyle, StyleObject } from "./types/core";
export type { NativeStyle, TwStyle } from "./types/runtime";
// Re-export colors
@@ -38,6 +38,7 @@ export {
// Re-export constants for customization
export { ASPECT_RATIO_PRESETS } from "./parser/aspectRatio";
export { COLORS } from "./parser/colors";
+export { BLUR_SCALE, DROP_SHADOW_SCALE } from "./parser/filters";
export { INSET_SCALE, Z_INDEX_SCALE } from "./parser/layout";
export { SHADOW_SCALE } from "./parser/shadows";
export { SIZE_PERCENTAGES, SIZE_SCALE } from "./parser/sizing";
diff --git a/src/parser/filters.test.ts b/src/parser/filters.test.ts
index 5f25994..d9305ee 100644
--- a/src/parser/filters.test.ts
+++ b/src/parser/filters.test.ts
@@ -1,47 +1,137 @@
import { describe, expect, it } from "vitest";
-import { parseFilter } from "./filters";
+import { applyOpacity } from "../utils/colorUtils";
+import { BLUR_SCALE, DROP_SHADOW_SCALE, parseFilter } from "./filters";
import { parseClassName } from "./index";
-describe("parseFilter - brightness", () => {
- it("should parse numeric brightness values as percentages", () => {
- expect(parseFilter("brightness-0")).toEqual({ filter: [{ brightness: 0 }] });
- expect(parseFilter("brightness-50")).toEqual({ filter: [{ brightness: 0.5 }] });
- expect(parseFilter("brightness-100")).toEqual({ filter: [{ brightness: 1 }] });
+describe("parseFilter - percentage filters", () => {
+ it("should parse numeric values as percentages", () => {
expect(parseFilter("brightness-101")).toEqual({ filter: [{ brightness: 1.01 }] });
- expect(parseFilter("brightness-125")).toEqual({ filter: [{ brightness: 1.25 }] });
- expect(parseFilter("brightness-200")).toEqual({ filter: [{ brightness: 2 }] });
+ expect(parseFilter("contrast-125")).toEqual({ filter: [{ contrast: 1.25 }] });
+ expect(parseFilter("grayscale-50")).toEqual({ filter: [{ grayscale: 0.5 }] });
+ expect(parseFilter("invert-25")).toEqual({ filter: [{ invert: 0.25 }] });
+ expect(parseFilter("saturate-150")).toEqual({ filter: [{ saturate: 1.5 }] });
+ expect(parseFilter("sepia-75")).toEqual({ filter: [{ sepia: 0.75 }] });
});
- it("should parse arbitrary numeric brightness values", () => {
+ it("should parse bare full-effect utilities", () => {
+ expect(parseFilter("grayscale")).toEqual({ filter: [{ grayscale: 1 }] });
+ expect(parseFilter("invert")).toEqual({ filter: [{ invert: 1 }] });
+ expect(parseFilter("sepia")).toEqual({ filter: [{ sepia: 1 }] });
+ });
+
+ it("should parse arbitrary numeric values", () => {
expect(parseFilter("brightness-[1.01]")).toEqual({ filter: [{ brightness: 1.01 }] });
- expect(parseFilter("brightness-[.5]")).toEqual({ filter: [{ brightness: 0.5 }] });
- expect(parseFilter("brightness-[2]")).toEqual({ filter: [{ brightness: 2 }] });
+ expect(parseFilter("contrast-[.5]")).toEqual({ filter: [{ contrast: 0.5 }] });
+ expect(parseFilter("saturate-[2]")).toEqual({ filter: [{ saturate: 2 }] });
});
- it("should parse arbitrary percentage brightness values", () => {
+ it("should parse arbitrary percentage values", () => {
expect(parseFilter("brightness-[80%]")).toEqual({ filter: [{ brightness: 0.8 }] });
- expect(parseFilter("brightness-[101%]")).toEqual({ filter: [{ brightness: 1.01 }] });
+ expect(parseFilter("grayscale-[25%]")).toEqual({ filter: [{ grayscale: 0.25 }] });
+ expect(parseFilter("sepia-[101%]")).toEqual({ filter: [{ sepia: 1.01 }] });
});
+});
- it("should reject unsupported or negative brightness values", () => {
- expect(parseFilter("brightness-[-1]")).toBeNull();
- expect(parseFilter("brightness-[1.01px]")).toBeNull();
- expect(parseFilter("brightness-[abc]")).toBeNull();
- expect(parseFilter("brightness-auto")).toBeNull();
- expect(parseFilter("brightness-")).toBeNull();
+describe("parseFilter - blur", () => {
+ it("should expose the Tailwind blur scale", () => {
+ expect(BLUR_SCALE).toEqual({ none: 0, xs: 4, sm: 8, md: 12, lg: 16, xl: 24, "2xl": 40, "3xl": 64 });
});
- it("should return null for unrelated classes", () => {
- expect(parseFilter("contrast-100")).toBeNull();
- expect(parseFilter("bg-white")).toBeNull();
- expect(parseFilter("")).toBeNull();
+ it("should parse blur presets", () => {
+ expect(parseFilter("blur-none")).toEqual({ filter: [{ blur: 0 }] });
+ expect(parseFilter("blur-xs")).toEqual({ filter: [{ blur: 4 }] });
+ expect(parseFilter("blur-sm")).toEqual({ filter: [{ blur: 8 }] });
+ expect(parseFilter("blur-3xl")).toEqual({ filter: [{ blur: 64 }] });
+ });
+
+ it("should parse arbitrary pixel blur values", () => {
+ expect(parseFilter("blur-[2px]")).toEqual({ filter: [{ blur: 2 }] });
+ expect(parseFilter("blur-[2.5]")).toEqual({ filter: [{ blur: 2.5 }] });
+ });
+});
+
+describe("parseFilter - hue rotate", () => {
+ it("should parse degree utilities", () => {
+ expect(parseFilter("hue-rotate-0")).toEqual({ filter: [{ hueRotate: "0deg" }] });
+ expect(parseFilter("hue-rotate-45")).toEqual({ filter: [{ hueRotate: "45deg" }] });
+ expect(parseFilter("-hue-rotate-90")).toEqual({ filter: [{ hueRotate: "-90deg" }] });
+ });
+
+ it("should parse arbitrary deg and rad angles", () => {
+ expect(parseFilter("hue-rotate-[22.5deg]")).toEqual({ filter: [{ hueRotate: "22.5deg" }] });
+ expect(parseFilter("hue-rotate-[-45deg]")).toEqual({ filter: [{ hueRotate: "-45deg" }] });
+ expect(parseFilter("hue-rotate-[0.5rad]")).toEqual({ filter: [{ hueRotate: "0.5rad" }] });
+ });
+});
+
+describe("parseFilter - drop shadow", () => {
+ it("should expose and parse Tailwind drop-shadow presets", () => {
+ expect(Object.keys(DROP_SHADOW_SCALE)).toEqual(["xs", "sm", "md", "lg", "xl", "2xl", "none"]);
+ expect(parseFilter("drop-shadow-md")).toEqual({ filter: [{ dropShadow: DROP_SHADOW_SCALE.md }] });
+ expect(parseFilter("drop-shadow-none")).toEqual({ filter: [{ dropShadow: DROP_SHADOW_SCALE.none }] });
+ });
+
+ it("should apply opacity modifiers to preset drop shadows", () => {
+ expect(parseFilter("drop-shadow-xl/50")).toEqual({
+ filter: [{ dropShadow: { ...DROP_SHADOW_SCALE.xl, color: applyOpacity("#000000", 50) } }],
+ });
+ });
+
+ it("should parse arbitrary drop shadows", () => {
+ expect(parseFilter("drop-shadow-[0_4px_4px_#00000080]")).toEqual({
+ filter: [{ dropShadow: { offsetX: 0, offsetY: 4, standardDeviation: 4, color: "#00000080" } }],
+ });
+ expect(parseFilter("drop-shadow-[-2px_3px_#ff0000]")).toEqual({
+ filter: [{ dropShadow: { offsetX: -2, offsetY: 3, color: "#ff0000" } }],
+ });
+ });
+
+ it("should resolve custom colors in arbitrary drop shadows", () => {
+ expect(parseFilter("drop-shadow-[0_2px_3px_brand]", { brand: "#123456" })).toEqual({
+ filter: [{ dropShadow: { offsetX: 0, offsetY: 2, standardDeviation: 3, color: "#123456" } }],
+ });
+ });
+});
+
+describe("parseFilter - composition and validation", () => {
+ it("should compose different filter functions", () => {
+ expect(parseClassName("blur-sm brightness-110 contrast-125 saturate-150")).toEqual({
+ filter: [{ blur: 8 }, { brightness: 1.1 }, { contrast: 1.25 }, { saturate: 1.5 }],
+ });
+ });
+
+ it("should use the last utility for duplicate filter functions", () => {
+ expect(parseClassName("brightness-110 blur-sm brightness-90")).toEqual({
+ filter: [{ brightness: 0.9 }, { blur: 8 }],
+ });
});
- it("should integrate with the class parser", () => {
+ it("should clear preceding filters with filter-none", () => {
+ expect(parseClassName("brightness-110 blur-sm filter-none")).toEqual({ filter: [] });
+ });
+
+ it("should compose native opacity with filters without a class collision", () => {
expect(parseClassName("brightness-[1.01] opacity-80")).toEqual({
filter: [{ brightness: 1.01 }],
opacity: 0.8,
});
});
+
+ it("should reject unsupported or malformed values", () => {
+ expect(parseFilter("brightness-[-1]")).toBeNull();
+ expect(parseFilter("contrast-[1.01px]")).toBeNull();
+ expect(parseFilter("blur-[-1px]")).toBeNull();
+ expect(parseFilter("blur-[50%]")).toBeNull();
+ expect(parseFilter("hue-rotate-[45turn]")).toBeNull();
+ expect(parseFilter("drop-shadow-[0_4px_-2px_#000000]")).toBeNull();
+ expect(parseFilter("drop-shadow-[invalid]")).toBeNull();
+ expect(parseFilter("brightness-auto")).toBeNull();
+ });
+
+ it("should return null for unrelated classes", () => {
+ expect(parseFilter("opacity-50")).toBeNull();
+ expect(parseFilter("bg-white")).toBeNull();
+ expect(parseFilter("")).toBeNull();
+ });
});
diff --git a/src/parser/filters.ts b/src/parser/filters.ts
index 1f2fdc7..0b24f14 100644
--- a/src/parser/filters.ts
+++ b/src/parser/filters.ts
@@ -3,15 +3,40 @@
*/
import type { StyleObject } from "../types";
+import type { DropShadowStyle, FilterStyle } from "../types/core";
+import { COLORS, applyOpacity, parseColorValue } from "../utils/colorUtils";
const NUMBER_PATTERN = String.raw`(?:\d+(?:\.\d*)?|\.\d+)`;
+export const BLUR_SCALE: Record = {
+ none: 0,
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+ "2xl": 40,
+ "3xl": 64,
+};
+
+export const DROP_SHADOW_SCALE: Record = {
+ xs: { offsetX: 0, offsetY: 1, standardDeviation: 1, color: applyOpacity(COLORS.black, 5) },
+ sm: { offsetX: 0, offsetY: 1, standardDeviation: 2, color: applyOpacity(COLORS.black, 15) },
+ md: { offsetX: 0, offsetY: 3, standardDeviation: 3, color: applyOpacity(COLORS.black, 12) },
+ lg: { offsetX: 0, offsetY: 4, standardDeviation: 4, color: applyOpacity(COLORS.black, 15) },
+ xl: { offsetX: 0, offsetY: 9, standardDeviation: 7, color: applyOpacity(COLORS.black, 10) },
+ "2xl": { offsetX: 0, offsetY: 25, standardDeviation: 25, color: applyOpacity(COLORS.black, 15) },
+ none: { offsetX: 0, offsetY: 0, standardDeviation: 0, color: "transparent" },
+};
+
+type PercentageFilterName = "brightness" | "contrast" | "grayscale" | "invert" | "saturate" | "sepia";
+
/**
- * Parse a Tailwind brightness amount.
- * Named numeric utilities are percentages (brightness-101 -> 1.01), while
- * bracketed numbers are raw React Native amounts (brightness-[1.01] -> 1.01).
+ * Parse a Tailwind percentage-based filter amount.
+ * Named numeric utilities are percentages (contrast-125 -> 1.25), while
+ * bracketed numbers are raw React Native amounts (contrast-[1.25] -> 1.25).
*/
-function parseBrightnessAmount(value: string): number | null {
+function parsePercentageAmount(value: string, filterName: PercentageFilterName): number | null {
const arbitraryMatch = value.match(new RegExp(`^\\[(${NUMBER_PATTERN})(%)?\\]$`));
if (arbitraryMatch) {
const amount = Number.parseFloat(arbitraryMatch[1]);
@@ -22,7 +47,7 @@ function parseBrightnessAmount(value: string): number | null {
/* v8 ignore next 5 */
if (process.env.NODE_ENV !== "production") {
console.warn(
- `[react-native-tailwind] Invalid arbitrary brightness value: ${value}. Only non-negative numbers and percentages are supported (e.g., [1.01], [80%]).`,
+ `[react-native-tailwind] Invalid arbitrary ${filterName} value: ${value}. Only non-negative numbers and percentages are supported (e.g., [1.01], [80%]).`,
);
}
return null;
@@ -35,15 +60,152 @@ function parseBrightnessAmount(value: string): number | null {
return null;
}
+function parseBlurAmount(value: string): number | null {
+ const scaleValue = BLUR_SCALE[value];
+ if (scaleValue !== undefined) {
+ return scaleValue;
+ }
+
+ const arbitraryMatch = value.match(new RegExp(`^\\[(${NUMBER_PATTERN})(?:px)?\\]$`));
+ if (arbitraryMatch) {
+ return Number.parseFloat(arbitraryMatch[1]);
+ }
+
+ if (value.startsWith("[") && value.endsWith("]")) {
+ /* v8 ignore next 5 */
+ if (process.env.NODE_ENV !== "production") {
+ console.warn(
+ `[react-native-tailwind] Invalid arbitrary blur value: ${value}. Only non-negative pixel values are supported (e.g., [2px], [2.5]).`,
+ );
+ }
+ }
+
+ return null;
+}
+
+function parseHueRotateAmount(value: string, isNegative: boolean): string | null {
+ const arbitraryMatch = value.match(new RegExp(`^\\[(-?${NUMBER_PATTERN})(deg|rad)\\]$`));
+ if (arbitraryMatch) {
+ const amount = Number.parseFloat(arbitraryMatch[1]);
+ return `${isNegative ? -amount : amount}${arbitraryMatch[2]}`;
+ }
+
+ if (value.startsWith("[") && value.endsWith("]")) {
+ /* v8 ignore next 5 */
+ if (process.env.NODE_ENV !== "production") {
+ console.warn(
+ `[react-native-tailwind] Invalid arbitrary hue-rotate value: ${value}. Only deg and rad angles are supported (e.g., [45deg], [0.5rad]).`,
+ );
+ }
+ return null;
+ }
+
+ if (new RegExp(`^${NUMBER_PATTERN}$`).test(value)) {
+ const amount = Number.parseFloat(value);
+ return `${isNegative ? -amount : amount}deg`;
+ }
+
+ return null;
+}
+
+function parseDropShadowLength(value: string, allowNegative: boolean): number | null {
+ const match = value.match(new RegExp(`^(${allowNegative ? "-?" : ""}${NUMBER_PATTERN})(?:px)?$`));
+ return match ? Number.parseFloat(match[1]) : null;
+}
+
+function parseDropShadowAmount(value: string, customColors?: Record): DropShadowStyle | null {
+ const opacityMatch = value.match(/^(.+)\/(\d+)$/);
+ if (opacityMatch) {
+ const preset = DROP_SHADOW_SCALE[opacityMatch[1]];
+ const opacity = Number.parseInt(opacityMatch[2], 10);
+ if (preset && opacity >= 0 && opacity <= 100) {
+ return { ...preset, color: applyOpacity(COLORS.black, opacity) };
+ }
+ return null;
+ }
+
+ const preset = DROP_SHADOW_SCALE[value];
+ if (preset) {
+ return preset;
+ }
+
+ if (value.startsWith("[") && value.endsWith("]")) {
+ const parts = value.slice(1, -1).split("_");
+ if (parts.length === 3 || parts.length === 4) {
+ const offsetX = parseDropShadowLength(parts[0], true);
+ const offsetY = parseDropShadowLength(parts[1], true);
+ const hasDeviation = parts.length === 4;
+ const standardDeviation = hasDeviation ? parseDropShadowLength(parts[2], false) : undefined;
+ const colorToken = parts[parts.length - 1];
+ const colorKey = colorToken.startsWith("#") ? `[${colorToken}]` : colorToken;
+ const color = parseColorValue(colorKey, customColors);
+
+ if (
+ offsetX !== null &&
+ offsetY !== null &&
+ (!hasDeviation || standardDeviation !== null) &&
+ color !== null
+ ) {
+ return { offsetX, offsetY, standardDeviation: standardDeviation ?? undefined, color };
+ }
+ }
+
+ /* v8 ignore next 5 */
+ if (process.env.NODE_ENV !== "production") {
+ console.warn(
+ `[react-native-tailwind] Invalid arbitrary drop-shadow value: ${value}. Use [offsetX_offsetY_blur_color] with pixel lengths and a supported color (e.g., [0_4px_4px_#00000080]).`,
+ );
+ }
+ }
+
+ return null;
+}
+
+function percentageFilterStyle(filterName: PercentageFilterName, amount: number): StyleObject {
+ return { filter: [{ [filterName]: amount } as FilterStyle] };
+}
+
/**
* Parse filter classes.
* @param cls - The class name to parse
*/
-export function parseFilter(cls: string): StyleObject | null {
- if (cls.startsWith("brightness-")) {
- const amount = parseBrightnessAmount(cls.substring(11));
+export function parseFilter(cls: string, customColors?: Record): StyleObject | null {
+ if (cls === "filter-none") {
+ return { filter: [] };
+ }
+
+ if (cls.startsWith("blur-")) {
+ const amount = parseBlurAmount(cls.substring(5));
+ if (amount !== null) {
+ return { filter: [{ blur: amount }] };
+ }
+ }
+
+ if (cls.startsWith("drop-shadow-")) {
+ const dropShadow = parseDropShadowAmount(cls.substring(12), customColors);
+ if (dropShadow !== null) {
+ return { filter: [{ dropShadow }] };
+ }
+ }
+
+ const hueRotateMatch = cls.match(/^(-?)hue-rotate-(.+)$/);
+ if (hueRotateMatch) {
+ const amount = parseHueRotateAmount(hueRotateMatch[2], hueRotateMatch[1] === "-");
+ if (amount !== null) {
+ return { filter: [{ hueRotate: amount }] };
+ }
+ }
+
+ if (cls === "grayscale" || cls === "invert" || cls === "sepia") {
+ return percentageFilterStyle(cls, 1);
+ }
+
+ const percentageFilterMatch = cls.match(/^(brightness|contrast|grayscale|invert|saturate|sepia)-(.+)$/);
+ if (percentageFilterMatch) {
+ const filterName = percentageFilterMatch[1] as PercentageFilterName;
+ const amount = parsePercentageAmount(percentageFilterMatch[2], filterName);
if (amount !== null) {
- return { filter: [{ brightness: amount }] };
+ return percentageFilterStyle(filterName, amount);
}
}
diff --git a/src/parser/index.ts b/src/parser/index.ts
index 1fbf197..e7d0d2f 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -64,7 +64,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
(cls: string) => parseSizing(cls, customTheme?.spacing),
(cls: string) => parseShadow(cls, customTheme?.colors),
- parseFilter,
+ (cls: string) => parseFilter(cls, customTheme?.colors),
parseAspectRatio,
(cls: string) => parseTransform(cls, customTheme?.spacing),
];
diff --git a/src/types/core.ts b/src/types/core.ts
index 06ad056..0c592f3 100644
--- a/src/types/core.ts
+++ b/src/types/core.ts
@@ -18,7 +18,22 @@ export type TransformStyle =
| { perspective?: number };
export type ShadowOffsetStyle = { width: number; height: number };
-export type FilterStyle = { brightness: number };
+export type DropShadowStyle = {
+ offsetX: number;
+ offsetY: number;
+ standardDeviation?: number;
+ color?: string;
+};
+export type FilterStyle =
+ | { brightness: number }
+ | { blur: number }
+ | { contrast: number }
+ | { grayscale: number }
+ | { hueRotate: string }
+ | { invert: number }
+ | { saturate: number }
+ | { sepia: number }
+ | { dropShadow: DropShadowStyle };
export type StyleObject = {
[key: string]: string | number | ShadowOffsetStyle | TransformStyle[] | FilterStyle[] | undefined;
diff --git a/src/utils/mergeStyles.test.ts b/src/utils/mergeStyles.test.ts
index 6a1a30b..9afa240 100644
--- a/src/utils/mergeStyles.test.ts
+++ b/src/utils/mergeStyles.test.ts
@@ -102,6 +102,29 @@ describe("mergeStyles", () => {
});
});
+ describe("filter array merging", () => {
+ it("should compose different filter functions", () => {
+ const target = { filter: [{ blur: 8 }] };
+ const source = { filter: [{ brightness: 1.1 }] };
+ expect(mergeStyles(target, source)).toEqual({
+ filter: [{ blur: 8 }, { brightness: 1.1 }],
+ });
+ });
+
+ it("should replace the same filter function with the last value", () => {
+ const target = { filter: [{ brightness: 1.1 }, { blur: 8 }] };
+ const source = { filter: [{ brightness: 0.9 }] };
+ expect(mergeStyles(target, source)).toEqual({
+ filter: [{ brightness: 0.9 }, { blur: 8 }],
+ });
+ });
+
+ it("should clear filters when the source array is empty", () => {
+ const target = { filter: [{ brightness: 1.1 }, { blur: 8 }] };
+ expect(mergeStyles(target, { filter: [] })).toEqual({ filter: [] });
+ });
+ });
+
describe("mixed properties", () => {
it("should handle mix of standard and transform properties", () => {
const target = { margin: 4, transform: [{ rotate: "45deg" }] };
diff --git a/src/utils/mergeStyles.ts b/src/utils/mergeStyles.ts
index 79144d3..3264f26 100644
--- a/src/utils/mergeStyles.ts
+++ b/src/utils/mergeStyles.ts
@@ -1,9 +1,9 @@
/**
* Smart merge utility for StyleObject values
- * Handles transform arrays with "last wins" semantics for same transform types
+ * Handles transform and filter arrays with "last wins" semantics for duplicate function types
*/
-import type { StyleObject, TransformStyle } from "../types/core";
+import type { FilterStyle, StyleObject, TransformStyle } from "../types/core";
/**
* Get the transform type key from a transform object
@@ -49,8 +49,37 @@ function mergeTransforms(target: TransformStyle[], source: TransformStyle[]): Tr
return result;
}
+function getFilterType(filter: FilterStyle): string {
+ return Object.keys(filter)[0];
+}
+
/**
- * Merge two StyleObject instances, handling transform arrays specially
+ * Merge native filter arrays, composing different filter functions while the
+ * last utility wins for duplicate filter types.
+ */
+function mergeFilters(target: FilterStyle[], source: FilterStyle[]): FilterStyle[] {
+ if (source.length === 0) {
+ return [];
+ }
+
+ const result: FilterStyle[] = [...target];
+
+ for (const sourceFilter of source) {
+ const sourceType = getFilterType(sourceFilter);
+ const existingIndex = result.findIndex((filter) => getFilterType(filter) === sourceType);
+
+ if (existingIndex !== -1) {
+ result[existingIndex] = sourceFilter;
+ } else {
+ result.push(sourceFilter);
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Merge two StyleObject instances, handling transform and filter arrays specially
*
* @param target - The target object to merge into (mutated)
* @param source - The source object to merge from
@@ -93,8 +122,16 @@ export function mergeStyles(target: StyleObject, source: StyleObject): StyleObje
// No existing array, just assign
target.transform = sourceTransforms;
}
+ } else if (key === "filter" && Array.isArray(sourceValue)) {
+ const sourceFilters = sourceValue as FilterStyle[];
+ const targetValue = target.filter;
+ if (Array.isArray(targetValue)) {
+ target.filter = mergeFilters(targetValue, sourceFilters);
+ } else {
+ target.filter = sourceFilters;
+ }
} else {
- // Standard Object.assign behavior for non-transform properties
+ // Standard Object.assign behavior for scalar and object properties
target[key] = sourceValue;
}
}
From a5e29fc00b21b243a54f8561ef7a488d2da420e5 Mon Sep 17 00:00:00 2001
From: Evan Simpson <25159851+e-simpson@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:35:07 -0400
Subject: [PATCH 3/4] fix: match Tailwind filter composition order
---
src/parser/filters.test.ts | 18 +++++++++++++++++-
src/parser/index.ts | 6 ++++++
src/utils/mergeStyles.test.ts | 19 ++++++++++++++++---
src/utils/mergeStyles.ts | 25 ++++++++++++++++++++++++-
4 files changed, 63 insertions(+), 5 deletions(-)
diff --git a/src/parser/filters.test.ts b/src/parser/filters.test.ts
index d9305ee..704cac2 100644
--- a/src/parser/filters.test.ts
+++ b/src/parser/filters.test.ts
@@ -103,7 +103,7 @@ describe("parseFilter - composition and validation", () => {
it("should use the last utility for duplicate filter functions", () => {
expect(parseClassName("brightness-110 blur-sm brightness-90")).toEqual({
- filter: [{ brightness: 0.9 }, { blur: 8 }],
+ filter: [{ blur: 8 }, { brightness: 0.9 }],
});
});
@@ -111,6 +111,22 @@ describe("parseFilter - composition and validation", () => {
expect(parseClassName("brightness-110 blur-sm filter-none")).toEqual({ filter: [] });
});
+ it("should let filter-none win regardless of class order", () => {
+ expect(parseClassName("filter-none brightness-110 blur-sm")).toEqual({ filter: [] });
+ });
+
+ it("should compose filters in Tailwind's canonical order", () => {
+ expect(parseClassName("drop-shadow-md sepia hue-rotate-45 brightness-110 blur-sm")).toEqual({
+ filter: [
+ { blur: 8 },
+ { brightness: 1.1 },
+ { hueRotate: "45deg" },
+ { sepia: 1 },
+ { dropShadow: DROP_SHADOW_SCALE.md },
+ ],
+ });
+ });
+
it("should compose native opacity with filters without a class collision", () => {
expect(parseClassName("brightness-[1.01] opacity-80")).toEqual({
filter: [{ brightness: 1.01 }],
diff --git a/src/parser/index.ts b/src/parser/index.ts
index e7d0d2f..3b99b92 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -42,6 +42,12 @@ export function parseClassName(className: string, customTheme?: CustomTheme): St
mergeStyles(style, parsedStyle);
}
+ // Tailwind emits filter-none after composable filter utilities, so it wins
+ // regardless of the order of classes in markup.
+ if (classes.includes("filter-none")) {
+ style.filter = [];
+ }
+
return style;
}
diff --git a/src/utils/mergeStyles.test.ts b/src/utils/mergeStyles.test.ts
index 9afa240..c725487 100644
--- a/src/utils/mergeStyles.test.ts
+++ b/src/utils/mergeStyles.test.ts
@@ -104,8 +104,8 @@ describe("mergeStyles", () => {
describe("filter array merging", () => {
it("should compose different filter functions", () => {
- const target = { filter: [{ blur: 8 }] };
- const source = { filter: [{ brightness: 1.1 }] };
+ const target = { filter: [{ brightness: 1.1 }] };
+ const source = { filter: [{ blur: 8 }] };
expect(mergeStyles(target, source)).toEqual({
filter: [{ blur: 8 }, { brightness: 1.1 }],
});
@@ -115,7 +115,7 @@ describe("mergeStyles", () => {
const target = { filter: [{ brightness: 1.1 }, { blur: 8 }] };
const source = { filter: [{ brightness: 0.9 }] };
expect(mergeStyles(target, source)).toEqual({
- filter: [{ brightness: 0.9 }, { blur: 8 }],
+ filter: [{ blur: 8 }, { brightness: 0.9 }],
});
});
@@ -123,6 +123,19 @@ describe("mergeStyles", () => {
const target = { filter: [{ brightness: 1.1 }, { blur: 8 }] };
expect(mergeStyles(target, { filter: [] })).toEqual({ filter: [] });
});
+
+ it("should use Tailwind's canonical order for composed filters", () => {
+ const target = { filter: [{ dropShadow: { offsetX: 0, offsetY: 1 } }, { sepia: 1 }] };
+ const source = { filter: [{ hueRotate: "45deg" }, { contrast: 1.25 }] };
+ expect(mergeStyles(target, source)).toEqual({
+ filter: [
+ { contrast: 1.25 },
+ { hueRotate: "45deg" },
+ { sepia: 1 },
+ { dropShadow: { offsetX: 0, offsetY: 1 } },
+ ],
+ });
+ });
});
describe("mixed properties", () => {
diff --git a/src/utils/mergeStyles.ts b/src/utils/mergeStyles.ts
index 3264f26..f66c44f 100644
--- a/src/utils/mergeStyles.ts
+++ b/src/utils/mergeStyles.ts
@@ -53,6 +53,29 @@ function getFilterType(filter: FilterStyle): string {
return Object.keys(filter)[0];
}
+// Tailwind composes independent filter utilities into one declaration in this
+// order. Native filter arrays are order-sensitive, so preserve the same order
+// regardless of class ordering.
+const FILTER_ORDER = [
+ "blur",
+ "brightness",
+ "contrast",
+ "grayscale",
+ "hueRotate",
+ "invert",
+ "saturate",
+ "sepia",
+ "dropShadow",
+] as const;
+
+function sortFilters(filters: FilterStyle[]): FilterStyle[] {
+ return filters.sort((left, right) => {
+ const leftIndex = FILTER_ORDER.indexOf(getFilterType(left) as (typeof FILTER_ORDER)[number]);
+ const rightIndex = FILTER_ORDER.indexOf(getFilterType(right) as (typeof FILTER_ORDER)[number]);
+ return leftIndex - rightIndex;
+ });
+}
+
/**
* Merge native filter arrays, composing different filter functions while the
* last utility wins for duplicate filter types.
@@ -75,7 +98,7 @@ function mergeFilters(target: FilterStyle[], source: FilterStyle[]): FilterStyle
}
}
- return result;
+ return sortFilters(result);
}
/**
From a60a097077ab92658bbad3d5b94e1a817af51850 Mon Sep 17 00:00:00 2001
From: Evan Simpson <25159851+e-simpson@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:08:13 -0400
Subject: [PATCH 4/4] docs: clarify filter-none precedence
---
docs/src/content/docs/reference/filters.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/src/content/docs/reference/filters.md b/docs/src/content/docs/reference/filters.md
index 3ab15e8..a05c633 100644
--- a/docs/src/content/docs/reference/filters.md
+++ b/docs/src/content/docs/reference/filters.md
@@ -119,10 +119,11 @@ Different filter utilities compile into one ordered native filter array. If the
// filter: [{ brightness: 0.9 }]
```
-Use `filter-none` to clear filters that appear before it:
+Use `filter-none` to clear all composable filters on the same element. It wins regardless of class-string order, matching Tailwind's generated CSS:
```tsx
// filter: []
+ // filter: []
```
Unsupported units, malformed drop shadows, and negative values where React Native requires non-negative amounts are ignored with a development warning.