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);