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
16 changes: 16 additions & 0 deletions docs/src/content/docs/guides/color-scheme.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,24 @@ The `scheme:` modifier only works with color utilities:
- ✅ `scheme:text-{color}` — Text colors
- ✅ `scheme:bg-{color}` — Background colors
- ✅ `scheme:border-{color}` — Border colors
- ✅ `scheme:outline-{color}` — Outline colors
- ❌ Other utilities — Ignored with development warning

Opacity modifiers are preserved when the semantic color expands:

```tsx
<View className="scheme:bg-primary/25" />
// dark:bg-primary-dark/25 light:bg-primary-light/25

<Text className="scheme:text-systemLabel/80" />
// dark:text-systemLabel-dark/80 light:text-systemLabel-light/80

<View className="scheme:bg-primary/[.37]" />
// dark:bg-primary-dark/[.37] light:bg-primary-light/[.37]
```

Named opacity modifiers use percentages from `/0` through `/100`. Arbitrary modifiers accept a raw alpha such as `/[.37]` or an explicit percentage such as `/[37%]`. If a configured color already includes an alpha channel, the modifier composes with that alpha instead of producing an invalid color.

### Use Cases

**Semantic color names:**
Expand Down
78 changes: 77 additions & 1 deletion docs/src/content/docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,82 @@ description: Access the parser and constants programmatically

Access the parser and constants programmatically for advanced use cases.

## Compile-Time Raw Colors

Use `twColor` for a static color string that can be declared at module scope. It accepts the same palette names, utility-form names, slash opacity, and arbitrary hex values as color utilities:

```tsx
import { twColor } from "@mgcrea/react-native-tailwind";

const blue = twColor`blue-500`;
const translucentBlue = twColor`blue-500/35`;
const customHex = twColor`[#50d71e]`;
```

Each tag is replaced with a string literal and has no runtime cost. Interpolations and `scheme:` tokens produce a compile error. A static tag cannot react to a runtime color scheme, so use `useTwColor` for scheme-aware colors.

Use `useTwColor` when a React Native API needs a reactive color string instead of a `style` object, such as navigation options, gradients, SVG, or Skia:

```tsx
import { useTwColor, useTwColors } from "@mgcrea/react-native-tailwind";

function Header() {
const tintColor = useTwColor("scheme:accent");

return <Icon color={tintColor} />;
}
```

Hook tokens use the same configured palette as color classes. Prefix a token with `scheme:` to resolve its configured light and dark variants reactively:

```tsx
const card = useTwColor("scheme:card");
const fallback = useTwColor("blue-500");
```

The Babel plugin replaces each call with literal color strings. Scheme tokens reuse the plugin's configured `colorScheme` hook, including custom application theme hooks.
When that hook returns `"dark"`, the dark variant is used; `"light"`, `null`, and other non-dark values use the light variant, matching the default light appearance before a dark preference is active.

Use `useTwColors` to resolve several named colors with one injected scheme hook:

```tsx
function Screen() {
const colors = useTwColors({
background: "scheme:background",
text: "scheme:text",
accent: "accent",
});

return (
<LinearGradient colors={[colors.background, colors.accent]}>
<Text style={{ color: colors.text }}>Hello</Text>
</LinearGradient>
);
}
```

Both helpers require static string literals and must be called unconditionally inside a function component. Unknown colors, unsupported modifiers, dynamic expressions, and module-scope calls produce a compile error. Utility-form tokens such as `bg-card` and `text-accent` are accepted, but raw palette names are preferred when the consumer needs only a string.

### Type Checking Custom Theme Tokens

The Babel plugin validates every token against the actual Tailwind configuration, but the exported TypeScript signature accepts `string` because a library declaration cannot automatically infer another project's Babel-loaded configuration or arbitrary-value grammar. Applications can keep config-derived autocomplete and typo checking with a direct `satisfies` expression:

```tsx
import tailwindConfig from "./tailwind.config";

type ThemeColor = keyof typeof tailwindConfig.theme.extend.colors;
type SchemeThemeColor = `scheme:${ThemeColor}`;

function Card() {
const background = useTwColor("scheme:card" satisfies SchemeThemeColor);
const colors = useTwColors({
text: "scheme:text" satisfies SchemeThemeColor,
});
}
```

TypeScript checks the literal against the application's config-derived union, then the Babel plugin removes the type-only expression and compiles the color normally. Keep the literal directly inside `useTwColor` or `useTwColors`; variables and wrapper functions are intentionally rejected so the compiler can prove the token statically.

## parseClassName

Parse className strings to React Native styles:
Expand Down Expand Up @@ -166,7 +242,7 @@ function ThemedComponent() {

## Important Notes

- The programmatic API parses styles at **runtime**, not compile-time
- `twColor`, `useTwColor`, and `useTwColors` are compile-time APIs; `parseClassName` parses styles at runtime
- For production apps, prefer using `className` prop for compile-time optimization
- Use the programmatic API for:
- Testing
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/outlines.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Utilities for controlling the outline style of an element.
<View className="outline-blue-500" /> // outlineColor: '#3B82F6'
<View className="outline-[#ff0000]" /> // outlineColor: '#ff0000'
<View className="outline-red-500/50" /> // outlineColor: '#EF4444' (50% opacity)
<View className="dark:outline-gray-200" /> // Theme-aware outline color
<View className="scheme:outline-brand" /> // Expands to light/dark variants
```

## Outline Style
Expand Down
14 changes: 14 additions & 0 deletions src/babel/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { jsxAttributeVisitor } from "./plugin/visitors/className.js";
import { importDeclarationVisitor } from "./plugin/visitors/imports.js";
import { programEnter, programExit } from "./plugin/visitors/program.js";
import { callExpressionVisitor, taggedTemplateVisitor } from "./plugin/visitors/tw.js";
import { twColorCallExpressionVisitor, twColorTaggedTemplateVisitor } from "./plugin/visitors/twColor.js";
import { scanForColorSchemeModifiers } from "./utils/preInjection.js";
import { injectColorSchemeHook } from "./utils/styleInjection.js";

Expand Down Expand Up @@ -80,6 +81,12 @@ export default function reactNativeTailwindBabelPlugin(
const importedName = spec.imported.name;
if (importedName === "tw" || importedName === "twStyle") {
state.twImportNames.add(spec.local.name);
} else if (
importedName === "twColor" ||
importedName === "useTwColor" ||
importedName === "useTwColors"
) {
state.twColorImportNames.set(spec.local.name, importedName);
}
}
}
Expand All @@ -100,6 +107,11 @@ export default function reactNativeTailwindBabelPlugin(
state.attributePatterns,
state.twImportNames,
t,
new Set(
[...state.twColorImportNames]
.filter(([, helper]) => helper !== "twColor")
.map(([localName]) => localName),
),
)
) {
injectColorSchemeHook(
Expand Down Expand Up @@ -130,10 +142,12 @@ export default function reactNativeTailwindBabelPlugin(

TaggedTemplateExpression(path, state) {
taggedTemplateVisitor(path, state, t);
twColorTaggedTemplateVisitor(path, state, t);
},

CallExpression(path, state) {
callExpressionVisitor(path, state, t);
twColorCallExpressionVisitor(path, state, t);
},

JSXAttribute(path, state) {
Expand Down
4 changes: 4 additions & 0 deletions src/babel/plugin/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export type PluginState = PluginPass & {
// Track tw/twStyle imports from main package
twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
hasTwImport: boolean;
twColorImportNames: Map<string, "twColor" | "useTwColor" | "useTwColors">;
hasTwColorImport: boolean;
// Track react-native import path for conditional StyleSheet/Platform injection
reactNativeImportPath?: NodePath<BabelTypes.ImportDeclaration>;
// Track function components that need colorScheme hook injection
Expand Down Expand Up @@ -179,6 +181,8 @@ export function createInitialState(
stylesIdentifier,
twImportNames: new Set(),
hasTwImport: false,
twColorImportNames: new Map(),
hasTwColorImport: false,
reactNativeImportPath: undefined,
functionComponentsNeedingColorScheme: new Set(),
functionComponentsNeedingWindowDimensions: new Set(),
Expand Down
6 changes: 6 additions & 0 deletions src/babel/plugin/visitors/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export function importDeclarationVisitor(
const localName = spec.local.name;
state.twImportNames.add(localName);
// Don't set hasTwImport yet - only set it when we successfully transform a call
} else if (
importedName === "twColor" ||
importedName === "useTwColor" ||
importedName === "useTwColors"
) {
state.twColorImportNames.set(spec.local.name, importedName);
}
}
});
Expand Down
4 changes: 4 additions & 0 deletions src/babel/plugin/visitors/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
injectStylesAtTop,
injectWindowDimensionsHook,
} from "../../utils/styleInjection.js";
import { removeTwColorImports } from "../../utils/twColorProcessing.js";
import { removeTwImports } from "../../utils/twProcessing.js";
import type { PluginState } from "../state.js";

Expand All @@ -41,6 +42,9 @@ export function programExit(
if (state.hasTwImport) {
removeTwImports(path, t);
}
if (state.hasTwColorImport) {
removeTwColorImports(path, t);
}

// If no classNames were found and no hooks/imports needed, skip processing
if (
Expand Down
Loading
Loading