diff --git a/README.md b/README.md
index bafb2302..36c21a21 100644
--- a/README.md
+++ b/README.md
@@ -214,26 +214,28 @@ It is preferable that all CSS variables are set via CSS. If you need values to c
}
```
-As a last resort, you can use `VariableContext` to dynamically set CSS variables in JavaScript
+As a last resort, you can use `VariableContextProvider` to dynamically set CSS variables in JavaScript
```ts
-import { VariableContext } from 'react-native-css';
+import { VariableContextProvider } from 'react-native-css';
export default function App() {
return (
-
+
Hello, world!
-
+
)
}
```
-This API only allows for setting CSS variables as primitive values. For more complex styles, you will need to use a helper CSS class.
+A custom property holds a token stream on web and a structured value on native, so this API accepts the values both platforms honour: a string, a number, a boolean, or an array of those (the `CustomPropertyValue` type).
+
+An array is a comma-separated CSS list — a `font-family` stack, a `transition-property` list. A value whose parts are separated by spaces, such as a `box-shadow`, is a single string. `undefined` leaves the property unset, so an ancestor's value inherits. Anything beyond that needs a helper CSS class.
> [!IMPORTANT]
-> By using `VariableContext` you may need to disable the `inlineVariable` optimization
+> By using `VariableContextProvider` you may need to disable the `inlineVariable` optimization
## Optimizations
diff --git a/src/__tests__/_custom-property-value.types.ts b/src/__tests__/_custom-property-value.types.ts
new file mode 100644
index 00000000..2c75a5fa
--- /dev/null
+++ b/src/__tests__/_custom-property-value.types.ts
@@ -0,0 +1,60 @@
+// Type-level assertions, run by yarn typecheck; the leading underscore keeps jest off this file
+import type { CustomPropertyValue } from "react-native-css";
+
+type RootApi = typeof import("react-native-css");
+type NativeApi = typeof import("react-native-css/native");
+type WebApi = typeof import("react-native-css/web");
+
+type Equal = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
+type Expect = T;
+type Accepts = [TValue] extends [TTarget] ? true : false;
+
+type VarsParameter unknown }> =
+ Parameters[0];
+type ProviderValue<
+ TApi extends { VariableContextProvider: (props: never) => unknown },
+> = Parameters[0]["value"];
+
+// The shared entry's declarations reach both platforms, so all three entries must agree
+export type Parity = [
+ Expect, Record>>,
+ Expect, VarsParameter>>,
+ Expect, VarsParameter>>,
+ Expect<
+ Equal, Record<`--${string}`, CustomPropertyValue>>
+ >,
+ Expect, ProviderValue>>,
+ Expect, ProviderValue>>,
+];
+
+export type Accepted = [
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+];
+
+export type Rejected = [
+ // `null` is not a CSS value — `undefined` is how a property is left unset.
+ Expect, false>>,
+ // An object has no custom-property serialisation.
+ Expect, false>>,
+ Expect<
+ Equal<
+ Accepts<[Record, "var", ["other"]], CustomPropertyValue>,
+ false
+ >
+ >,
+ Expect<
+ Equal<
+ Accepts<
+ import("react-native-css/compiler").StyleDescriptor,
+ CustomPropertyValue
+ >,
+ false
+ >
+ >,
+];
diff --git a/src/__tests__/native/vars.test.tsx b/src/__tests__/native/vars.test.tsx
index d87eb7c4..b893a77c 100644
--- a/src/__tests__/native/vars.test.tsx
+++ b/src/__tests__/native/vars.test.tsx
@@ -36,3 +36,40 @@ test("vars", () => {
color: "blue",
});
});
+
+test("vars: an array is a list the declaration consumes", () => {
+ registerCSS(`.my-class { font-variant-caps: var(--font-variant); }`);
+
+ render(
+ ,
+ );
+
+ // fontVariant takes an array, so the list must survive the variable pipeline as a list
+ expect(screen.getByTestId(testID).props.style).toStrictEqual({
+ fontVariant: ["small-caps"],
+ });
+});
+
+test("vars: undefined leaves the property unset", () => {
+ registerCSS(`
+ .my-class { color: var(--color); }
+ .green { --color: green; }
+ `);
+
+ render(
+ ,
+ );
+
+ // An unset inline value lets the class's --color through
+ expect(screen.getByTestId(testID).props.style).toStrictEqual({
+ color: "#008000",
+ });
+});
diff --git a/src/__tests__/web/variables.test.tsx b/src/__tests__/web/variables.test.tsx
new file mode 100644
index 00000000..be4e2876
--- /dev/null
+++ b/src/__tests__/web/variables.test.tsx
@@ -0,0 +1,56 @@
+import { View } from "react-native";
+
+import { render, screen } from "@testing-library/react-native";
+
+import { VariableContextProvider } from "../../web/api";
+
+const testID = "react-native-css";
+
+/** The custom properties the provider wrote onto its host element's style. */
+const customProperties = (
+ value: Parameters[0]["value"],
+): unknown => {
+ render(
+
+
+ ,
+ );
+
+ return screen.root.props.style;
+};
+
+test("an array is a comma-separated CSS list", () => {
+ expect(customProperties({ "--font-stack": ["Inter", "Helvetica"] })).toEqual({
+ "display": "contents",
+ "--font-stack": "Inter,Helvetica",
+ });
+});
+
+test("a nested array flattens into the same list", () => {
+ expect(customProperties({ "--list": [1, ["a", true]] })).toEqual({
+ "display": "contents",
+ "--list": "1,a,true",
+ });
+});
+
+test("undefined leaves the property unset", () => {
+ expect(customProperties({ "--set": "red", "--unset": undefined })).toEqual({
+ "display": "contents",
+ "--set": "red",
+ });
+});
+
+test("an undefined member drops out of the list", () => {
+ expect(customProperties({ "--list": ["a", undefined, "b"] })).toEqual({
+ "display": "contents",
+ "--list": "a,b",
+ });
+});
+
+test("scalars serialise to their token form", () => {
+ expect(customProperties({ "--number": 1, "--boolean": true })).toEqual({
+ "display": "contents",
+ "--number": "1",
+ "--boolean": "true",
+ });
+});
diff --git a/src/native-internal/variables.tsx b/src/native-internal/variables.tsx
index 8a0f81e0..a098135a 100644
--- a/src/native-internal/variables.tsx
+++ b/src/native-internal/variables.tsx
@@ -5,9 +5,8 @@ import {
type PropsWithChildren,
} from "react";
-import type { StyleDescriptor } from "react-native-css/compiler";
-
import { VAR_SYMBOL, type VariableContextValue } from "../native/reactivity";
+import type { CustomPropertyValue } from "../runtime.types";
globalThis.__react_native_css_variable_context ??=
createContext({
@@ -17,7 +16,9 @@ globalThis.__react_native_css_variable_context ??=
export const VariableContext = globalThis.__react_native_css_variable_context;
export function VariableContextProvider(
- props: PropsWithChildren<{ value: Record<`--${string}`, StyleDescriptor> }>,
+ props: PropsWithChildren<{
+ value: Record<`--${string}`, CustomPropertyValue>;
+ }>,
) {
const inheritedVariables = useContext(VariableContext);
diff --git a/src/native/api.tsx b/src/native/api.tsx
index 6736f746..481ba3d8 100644
--- a/src/native/api.tsx
+++ b/src/native/api.tsx
@@ -2,11 +2,11 @@
import { useContext, useEffect, useState, type ComponentType } from "react";
import { Appearance, type ViewStyle } from "react-native";
-import type { StyleDescriptor } from "react-native-css/compiler";
import { VariableContext } from "react-native-css/native-internal";
import type {
ColorScheme,
+ CustomPropertyValue,
Props,
ReactComponent,
Styled,
@@ -120,7 +120,7 @@ export function useNativeVariable(name: string) {
/**
* @deprecated Use `` instead.
*/
-export function vars(variables: Record): ViewStyle {
+export function vars(variables: Record): ViewStyle {
return Object.assign(
{ [VAR_SYMBOL]: "inline" },
Object.fromEntries(
diff --git a/src/runtime.types.ts b/src/runtime.types.ts
index 69cab6ca..093ef8c9 100644
--- a/src/runtime.types.ts
+++ b/src/runtime.types.ts
@@ -138,6 +138,16 @@ export type InlineStyle =
| (Record | undefined | null)[]
| (() => unknown);
+/******************************* Variables ******************************/
+
+// The values both platforms honour: web serialises them to a token stream, an array as a comma-separated list
+export type CustomPropertyValue =
+ | string
+ | number
+ | boolean
+ | undefined
+ | CustomPropertyValue[];
+
/********************************* Misc *********************************/
export type Props = Record | undefined | null;
diff --git a/src/web/api.tsx b/src/web/api.tsx
index 28e09ab8..81271877 100644
--- a/src/web/api.tsx
+++ b/src/web/api.tsx
@@ -13,7 +13,11 @@ import type {
StyledOptions,
} from "react-native-css";
-import type { ReactComponent, Styled } from "../runtime.types";
+import type {
+ CustomPropertyValue,
+ ReactComponent,
+ Styled,
+} from "../runtime.types";
import { assignStyle } from "./assign-style";
const defaultMapping: StyledConfiguration> = {
@@ -72,34 +76,54 @@ export const colorScheme: ColorScheme = {
},
};
-/**
- * @deprecated Use `` instead.
- */
-export function vars(variables: Record) {
- const $variables: Record = {};
+// undefined leaves the property unset, and drops out of a list
+function serializeCustomProperty(
+ value: CustomPropertyValue,
+): string | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ if (Array.isArray(value)) {
+ return value
+ .map((member) => serializeCustomProperty(member))
+ .filter((member) => member !== undefined)
+ .join(",");
+ }
+
+ return String(value);
+}
+
+function toCustomProperties(variables: Record) {
+ const properties: Record = {};
for (const [key, value] of Object.entries(variables)) {
- if (key.startsWith("--")) {
- $variables[key] = value.toString();
- } else {
- $variables[`--${key}`] = value.toString();
+ const serialized = serializeCustomProperty(value);
+
+ if (serialized !== undefined) {
+ properties[key.startsWith("--") ? key : `--${key}`] = serialized;
}
}
- return $variables;
+
+ return properties;
+}
+
+/**
+ * @deprecated Use `` instead.
+ */
+export function vars(variables: Record) {
+ return toCustomProperties(variables);
}
export function VariableContextProvider(
- props: PropsWithChildren<{ value: Record<`--${string}`, string | number> }>,
+ props: PropsWithChildren<{
+ value: Record<`--${string}`, CustomPropertyValue>;
+ }>,
) {
const style = useMemo(() => {
return {
display: "contents",
- ...Object.fromEntries(
- Object.entries(props.value).map(([key, value]) => [
- key.startsWith("--") ? key : `--${key}`,
- value,
- ]),
- ),
+ ...toCustomProperties(props.value),
};
}, [props.value]);