diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 5df770e..d6e7a0b 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -55,6 +55,7 @@ export default defineConfig({
{ label: "Colors", slug: "reference/colors" },
{ label: "Typography", slug: "reference/typography" },
{ label: "Borders", slug: "reference/borders" },
+ { label: "Outlines", slug: "reference/outlines" },
{ label: "Shadows & Elevation", slug: "reference/shadows" },
{ label: "Aspect Ratio", slug: "reference/aspect-ratio" },
{ label: "Transforms", slug: "reference/transforms" },
diff --git a/docs/src/content/docs/reference/borders.md b/docs/src/content/docs/reference/borders.md
index a63627f..9362a1c 100644
--- a/docs/src/content/docs/reference/borders.md
+++ b/docs/src/content/docs/reference/borders.md
@@ -167,4 +167,5 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ
## Related
- [Colors](/react-native-tailwind/reference/colors/) - Border color utilities
+- [Outlines](/react-native-tailwind/reference/outlines/) - Outline width, style, and offset utilities
- [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation
diff --git a/docs/src/content/docs/reference/outlines.md b/docs/src/content/docs/reference/outlines.md
new file mode 100644
index 0000000..7593e08
--- /dev/null
+++ b/docs/src/content/docs/reference/outlines.md
@@ -0,0 +1,59 @@
+---
+title: Outlines
+description: Outline width, style, and offset utilities
+---
+
+Utilities for controlling the outline style of an element.
+
+> **Note**: Outline support requires React Native 0.73+ (New Architecture) and setting the `outline` style property.
+
+## Outline Width
+
+```tsx
+ // outlineWidth: 1, outlineStyle: 'solid'
+ // outlineWidth: 0
+ // outlineWidth: 2
+ // outlineWidth: 4
+ // outlineWidth: 2
+ // outlineWidth: 0
+```
+
+## Outline Color
+
+```tsx
+ // outlineColor: '#3B82F6'
+ // outlineColor: '#ff0000'
+ // outlineColor: '#EF4444' (50% opacity)
+```
+
+## Outline Style
+
+```tsx
+ // outlineStyle: 'solid'
+ // outlineStyle: 'dashed'
+ // outlineStyle: 'dotted'
+```
+
+## Outline Offset
+
+Utilities for controlling the offset of an element's outline.
+
+```tsx
+ // outlineOffset: 0
+ // outlineOffset: 1
+ // outlineOffset: 2
+ // outlineOffset: 4
+ // outlineOffset: 3
+```
+
+## Example
+
+```tsx
+
+```
+
+## Related
+
+- [Borders](/react-native-tailwind/reference/borders/) - Border width, radius, and style utilities
+- [Colors](/react-native-tailwind/reference/colors/) - Color utilities
+- [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation
diff --git a/src/index.ts b/src/index.ts
index 515af19..0bd46f6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -25,6 +25,7 @@ export {
parseBorder,
parseColor,
parseLayout,
+ parseOutline,
parsePlaceholderClass,
parsePlaceholderClasses,
parseShadow,
diff --git a/src/parser/colors.ts b/src/parser/colors.ts
index 3966a00..b23e7ab 100644
--- a/src/parser/colors.ts
+++ b/src/parser/colors.ts
@@ -12,7 +12,10 @@ export { COLORS };
* Parse color classes (background, text, border)
* Supports opacity modifier: bg-blue-500/50, text-black/80, border-red-500/30
*/
-export function parseColor(cls: string, customColors?: Record): StyleObject | null {
+export function parseColor(
+ cls: string,
+ customColors?: Record
+): StyleObject | null {
// Helper to get color with custom override (custom colors take precedence)
const getColor = (key: string): string | undefined => {
return customColors?.[key] ?? COLORS[key];
@@ -32,7 +35,7 @@ export function parseColor(cls: string, customColors?: Record):
/* v8 ignore next 5 */
if (process.env.NODE_ENV !== "production") {
console.warn(
- `[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`,
+ `[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`
);
}
return null;
@@ -65,7 +68,7 @@ export function parseColor(cls: string, customColors?: Record):
/* v8 ignore next 5 */
if (process.env.NODE_ENV !== "production") {
console.warn(
- `[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`,
+ `[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`
);
}
return null;
@@ -116,6 +119,29 @@ export function parseColor(cls: string, customColors?: Record):
}
}
+ // Outline color: outline-blue-500, outline-blue-500/50, outline-[#ff0000]/80
+ if (
+ cls.startsWith("outline-") &&
+ !cls.match(/^outline-[0-9]/) &&
+ !cls.startsWith("outline-offset-")
+ ) {
+ const colorKey = cls.substring(8); // "outline-".length = 8
+
+ // Skip outline-style values
+ if (["solid", "dashed", "dotted", "none"].includes(colorKey)) {
+ return null;
+ }
+
+ // Skip arbitrary values that don't look like colors (e.g., outline-[3px] is width)
+ if (colorKey.startsWith("[") && !colorKey.startsWith("[#")) {
+ return null;
+ }
+ const color = parseColorWithOpacity(colorKey);
+ if (color) {
+ return { outlineColor: color };
+ }
+ }
+
// Directional border colors: border-t-red-500, border-l-blue-500/50, border-r-[#ff0000]
const dirBorderMatch = cls.match(/^border-([trblxy])-(.+)$/);
if (dirBorderMatch) {
diff --git a/src/parser/index.ts b/src/parser/index.ts
index ebc143a..0e89fcc 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -9,6 +9,7 @@ import { parseAspectRatio } from "./aspectRatio";
import { parseBorder } from "./borders";
import { parseColor } from "./colors";
import { parseLayout } from "./layout";
+import { parseOutline } from "./outline";
import { parseShadow } from "./shadows";
import { parseSizing } from "./sizing";
import { parseSpacing } from "./spacing";
@@ -56,6 +57,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
const parsers: Array<(cls: string) => StyleObject | null> = [
(cls: string) => parseSpacing(cls, customTheme?.spacing),
(cls: string) => parseBorder(cls, customTheme?.colors),
+ parseOutline,
(cls: string) => parseColor(cls, customTheme?.colors),
(cls: string) => parseLayout(cls, customTheme?.spacing),
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
@@ -86,6 +88,7 @@ export { parseAspectRatio } from "./aspectRatio";
export { parseBorder } from "./borders";
export { parseColor } from "./colors";
export { parseLayout } from "./layout";
+export { parseOutline } from "./outline";
export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
export { parseShadow } from "./shadows";
export { parseSizing } from "./sizing";
diff --git a/src/parser/outline.test.ts b/src/parser/outline.test.ts
new file mode 100644
index 0000000..56e396d
--- /dev/null
+++ b/src/parser/outline.test.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from "vitest";
+import { parseOutline } from "./outline";
+
+describe("parseOutline", () => {
+ it("should parse outline shorthand", () => {
+ expect(parseOutline("outline")).toEqual({
+ outlineWidth: 1,
+ outlineStyle: "solid",
+ });
+ });
+
+ it("should parse outline-none", () => {
+ expect(parseOutline("outline-none")).toEqual({ outlineWidth: 0 });
+ });
+
+ it("should parse outline width with preset values", () => {
+ expect(parseOutline("outline-0")).toEqual({ outlineWidth: 0 });
+ expect(parseOutline("outline-2")).toEqual({ outlineWidth: 2 });
+ expect(parseOutline("outline-4")).toEqual({ outlineWidth: 4 });
+ expect(parseOutline("outline-8")).toEqual({ outlineWidth: 8 });
+ });
+
+ it("should parse outline width with arbitrary values", () => {
+ expect(parseOutline("outline-[5px]")).toEqual({ outlineWidth: 5 });
+ expect(parseOutline("outline-[10]")).toEqual({ outlineWidth: 10 });
+ });
+
+ it("should parse outline style", () => {
+ expect(parseOutline("outline-solid")).toEqual({ outlineStyle: "solid" });
+ expect(parseOutline("outline-dashed")).toEqual({ outlineStyle: "dashed" });
+ expect(parseOutline("outline-dotted")).toEqual({ outlineStyle: "dotted" });
+ });
+
+ it("should parse outline offset with preset values", () => {
+ expect(parseOutline("outline-offset-0")).toEqual({ outlineOffset: 0 });
+ expect(parseOutline("outline-offset-2")).toEqual({ outlineOffset: 2 });
+ expect(parseOutline("outline-offset-4")).toEqual({ outlineOffset: 4 });
+ expect(parseOutline("outline-offset-8")).toEqual({ outlineOffset: 8 });
+ });
+
+ it("should parse outline offset with arbitrary values", () => {
+ expect(parseOutline("outline-offset-[3px]")).toEqual({ outlineOffset: 3 });
+ expect(parseOutline("outline-offset-[5]")).toEqual({ outlineOffset: 5 });
+ });
+
+ it("should return null for invalid outline values", () => {
+ expect(parseOutline("outline-invalid")).toBeNull();
+ expect(parseOutline("outline-3")).toBeNull(); // Not in scale
+ expect(parseOutline("outline-offset-3")).toBeNull(); // Not in scale
+ expect(parseOutline("outline-[5rem]")).toBeNull(); // Unsupported unit
+ });
+
+ it("should return null for outline colors (handled by parseColor)", () => {
+ expect(parseOutline("outline-red-500")).toBeNull();
+ expect(parseOutline("outline-[#ff0000]")).toBeNull();
+ });
+});
diff --git a/src/parser/outline.ts b/src/parser/outline.ts
new file mode 100644
index 0000000..d1e04e7
--- /dev/null
+++ b/src/parser/outline.ts
@@ -0,0 +1,101 @@
+/**
+ * Outline utilities (outline width, style, offset)
+ */
+
+import type { StyleObject } from "../types";
+import { BORDER_WIDTH_SCALE } from "./borders";
+
+/**
+ * Parse arbitrary outline width/offset value: [8px], [4]
+ * Returns number for px values, null for unsupported formats
+ */
+function parseArbitraryOutlineValue(value: string): number | null {
+ // Match: [8px] or [8] (pixels only)
+ const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
+ if (pxMatch) {
+ return parseInt(pxMatch[1], 10);
+ }
+
+ // Warn about unsupported formats
+ if (value.startsWith("[") && value.endsWith("]")) {
+ /* v8 ignore next 5 */
+ if (process.env.NODE_ENV !== "production") {
+ console.warn(
+ `[react-native-tailwind] Unsupported arbitrary outline value: ${value}. Only px values are supported (e.g., [8px] or [8]).`,
+ );
+ }
+ return null;
+ }
+
+ return null;
+}
+
+/**
+ * Parse outline classes
+ * @param cls - The class name to parse
+ */
+export function parseOutline(cls: string): StyleObject | null {
+ // Shorthand: outline (width: 1, style: solid)
+ if (cls === "outline") {
+ return { outlineWidth: 1, outlineStyle: "solid" };
+ }
+
+ // Outline none
+ if (cls === "outline-none") {
+ return { outlineWidth: 0 };
+ }
+
+ // Outline style
+ if (cls === "outline-solid") return { outlineStyle: "solid" };
+ if (cls === "outline-dotted") return { outlineStyle: "dotted" };
+ if (cls === "outline-dashed") return { outlineStyle: "dashed" };
+
+ // Outline offset: outline-offset-2, outline-offset-[3px]
+ if (cls.startsWith("outline-offset-")) {
+ const valueStr = cls.substring(15); // "outline-offset-".length = 15
+
+ // Try arbitrary value first
+ if (valueStr.startsWith("[")) {
+ const arbitraryValue = parseArbitraryOutlineValue(valueStr);
+ if (arbitraryValue !== null) {
+ return { outlineOffset: arbitraryValue };
+ }
+ return null;
+ }
+
+ // Try preset scale (reuse border width scale for consistency with default Tailwind)
+ const scaleValue = BORDER_WIDTH_SCALE[valueStr];
+ if (scaleValue !== undefined) {
+ return { outlineOffset: scaleValue };
+ }
+
+ return null;
+ }
+
+ // Outline width: outline-0, outline-2, outline-[5px]
+ // Must handle potential collision with outline-red-500 (colors)
+ // Logic: if it matches width pattern, return width. If it looks like color, return null (let parseColor handle it)
+
+ const widthMatch = cls.match(/^outline-(\d+)$/);
+ if (widthMatch) {
+ const value = BORDER_WIDTH_SCALE[widthMatch[1]];
+ if (value !== undefined) {
+ return { outlineWidth: value };
+ }
+ }
+
+ const arbMatch = cls.match(/^outline-(\[.+\])$/);
+ if (arbMatch) {
+ // Check if it's a color first? No, colors usually look like [#...] or [rgb(...)]
+ // parseArbitraryOutlineValue only accepts [123] or [123px]
+ // If it fails, it might be a color, so we return null
+ const arbitraryValue = parseArbitraryOutlineValue(arbMatch[1]);
+ if (arbitraryValue !== null) {
+ return { outlineWidth: arbitraryValue };
+ }
+ return null;
+ }
+
+ // If it's outline-{color}, return null so parseColor (called later in index.ts) handles it
+ return null;
+}