Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions docs/src/content/docs/reference/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ Control layout with flexbox, positioning, and display utilities.
<View className="overflow-scroll" /> // 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
<View className="opacity-65" /> // opacity: 0.65
<View className="opacity-[.67]" /> // opacity: 0.67
<View className="opacity-[67%]" /> // opacity: 0.67
```

Arbitrary values must resolve to React Native's supported range from `0` through `1`.

## Display

```tsx
Expand Down
14 changes: 14 additions & 0 deletions src/babel/plugin/visitors/className.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <View className="opacity-[.67]" />;
}
`;

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';
Expand Down
15 changes: 15 additions & 0 deletions src/parser/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
34 changes: 34 additions & 0 deletions src/parser/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, StyleObject> = {
flex: { display: "flex" },
Expand Down Expand Up @@ -244,6 +271,13 @@ export function parseLayout(cls: string, customSpacing?: Record<string, number>)
// 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);
Expand Down
Loading