From e6f54cae1a7d3f2a6b958574ed8c32a30baf976a Mon Sep 17 00:00:00 2001 From: Evan Simpson <25159851+e-simpson@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:30:01 -0400 Subject: [PATCH] feat: support arbitrary opacity values --- docs/src/content/docs/reference/layout.md | 12 ++++++++ src/babel/plugin/visitors/className.test.ts | 14 +++++++++ src/parser/layout.test.ts | 15 +++++++++ src/parser/layout.ts | 34 +++++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/docs/src/content/docs/reference/layout.md b/docs/src/content/docs/reference/layout.md index a592377..c145379 100644 --- a/docs/src/content/docs/reference/layout.md +++ b/docs/src/content/docs/reference/layout.md @@ -89,6 +89,18 @@ Control layout with flexbox, positioning, and display utilities. // overflow: 'scroll' ``` +## Opacity + +Named opacity utilities use the existing five-point percentage scale. Use an arbitrary raw value or percentage for an exact value: + +```tsx + // opacity: 0.65 + // opacity: 0.67 + // opacity: 0.67 +``` + +Arbitrary values must resolve to React Native's supported range from `0` through `1`. + ## Display ```tsx diff --git a/src/babel/plugin/visitors/className.test.ts b/src/babel/plugin/visitors/className.test.ts index 0f9a484..64aefd7 100644 --- a/src/babel/plugin/visitors/className.test.ts +++ b/src/babel/plugin/visitors/className.test.ts @@ -23,6 +23,20 @@ describe("className visitor - basic transformation", () => { expect(output).toContain("style:"); }); + it("should compile arbitrary opacity values", () => { + 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("opacity: 0.67"); + }); + it("should work with both tw and className in same file", () => { const input = ` import { tw } from '@mgcrea/react-native-tailwind'; diff --git a/src/parser/layout.test.ts b/src/parser/layout.test.ts index 0238c8b..21942e9 100644 --- a/src/parser/layout.test.ts +++ b/src/parser/layout.test.ts @@ -294,6 +294,21 @@ describe("parseLayout - opacity utilities", () => { expect(parseLayout("opacity-90")).toEqual({ opacity: 0.9 }); expect(parseLayout("opacity-95")).toEqual({ opacity: 0.95 }); }); + + it("should parse arbitrary raw and percentage opacity values", () => { + expect(parseLayout("opacity-[.67]")).toEqual({ opacity: 0.67 }); + expect(parseLayout("opacity-[0.125]")).toEqual({ opacity: 0.125 }); + expect(parseLayout("opacity-[1]")).toEqual({ opacity: 1 }); + expect(parseLayout("opacity-[67%]")).toEqual({ opacity: 0.67 }); + expect(parseLayout("opacity-[12.5%]")).toEqual({ opacity: 0.125 }); + }); + + it("should reject arbitrary opacity outside the native range", () => { + expect(parseLayout("opacity-[1.1]")).toBeNull(); + expect(parseLayout("opacity-[101%]")).toBeNull(); + expect(parseLayout("opacity-[-.1]")).toBeNull(); + expect(parseLayout("opacity-[invalid]")).toBeNull(); + }); }); describe("parseLayout - edge cases", () => { diff --git a/src/parser/layout.ts b/src/parser/layout.ts index 5309e22..01a1121 100644 --- a/src/parser/layout.ts +++ b/src/parser/layout.ts @@ -85,6 +85,33 @@ function parseArbitraryGrowShrink(value: string): number | null { return null; } +/** + * Parse an arbitrary opacity value: [.67], [0.67], [1], [67%]. + * React Native expects a normalized number from 0 to 1. + */ +function parseArbitraryOpacity(value: string): number | null { + const match = value.match(/^\[(\d+(?:\.\d*)?|\.\d+)(%)?\]$/); + if (match) { + const parsed = Number.parseFloat(match[1]); + const opacity = match[2] === "%" ? parsed / 100 : parsed; + if (opacity >= 0 && opacity <= 1) { + return opacity; + } + } + + if (value.startsWith("[") && value.endsWith("]")) { + /* v8 ignore next 5 */ + if (process.env.NODE_ENV !== "production") { + console.warn( + `[react-native-tailwind] Invalid arbitrary opacity value: ${value}. ` + + "Use a raw value from 0 to 1 or a percentage from 0% to 100% (e.g., [.67], [0.67], [67%]).", + ); + } + } + + return null; +} + // Display utilities const DISPLAY_MAP: Record = { flex: { display: "flex" }, @@ -244,6 +271,13 @@ export function parseLayout(cls: string, customSpacing?: Record) // Merge custom spacing with defaults for inset utilities const insetMap = customSpacing ? { ...INSET_SCALE, ...customSpacing } : INSET_SCALE; + if (cls.startsWith("opacity-")) { + const arbitraryOpacity = parseArbitraryOpacity(cls.substring(8)); + if (arbitraryOpacity !== null) { + return { opacity: arbitraryOpacity }; + } + } + // Z-index: z-0, z-10, z-20, z-[999], etc. if (cls.startsWith("z-")) { const zKey = cls.substring(2);