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..37077845
--- /dev/null
+++ b/src/__tests__/_custom-property-value.types.ts
@@ -0,0 +1,85 @@
+/**
+ * Compile-time assertions for the value a CSS custom property can be given from
+ * JavaScript. `yarn typecheck` is the runner — there is nothing here to execute,
+ * which is why the filename is `_`-prefixed and jest skips it.
+ *
+ * The planes are reached as module types rather than imported, so the file stays
+ * type-only under `verbatimModuleSyntax`.
+ */
+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 parity invariant. `react-native-css` ships one set of declarations to both
+ * platforms — there is no `moduleSuffixes` or export condition that hands a
+ * consumer the native ones — so a value the shared entry accepts has to be a
+ * value both implementations honour. A future divergence fails here.
+ */
+export type Parity = [
+ Expect, Record>>,
+ Expect, VarsParameter>>,
+ Expect, VarsParameter>>,
+ Expect<
+ Equal, Record<`--${string}`, CustomPropertyValue>>
+ >,
+ Expect, ProviderValue>>,
+ Expect, ProviderValue>>,
+];
+
+/**
+ * An array is a comma-separated CSS list, alongside the scalars a custom
+ * property can hold. `undefined` leaves the property unset so an ancestor's
+ * value inherits.
+ */
+export type Accepted = [
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+];
+
+/**
+ * A `StyleDescriptor` is wider than a custom property's public value type. Each
+ * row fails if the type is widened past what both implementations serialise.
+ */
+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>>,
+ // A StyleFunction is the compiler's own encoding of `var()`, `rgba()` and the
+ // rest. The web implementation has no way to serialise one.
+ Expect<
+ Equal<
+ Accepts<[Record, "var", ["other"]], CustomPropertyValue>,
+ false
+ >
+ >,
+ // The compiler's whole descriptor union — what the native runtime resolves
+ // internally — is wider than what a caller may hand in.
+ 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..ddcadba8 100644
--- a/src/__tests__/native/vars.test.tsx
+++ b/src/__tests__/native/vars.test.tsx
@@ -36,3 +36,42 @@ 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` is one of the React Native style properties that takes an
+ // array, so the list has to 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(
+ ,
+ );
+
+ // The class still sets `--color`, so leaving the inline value unset lets it
+ // through rather than blanking the property.
+ 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 3d68a3aa..3b738417 100644
--- a/src/native/api.tsx
+++ b/src/native/api.tsx
@@ -2,11 +2,11 @@
import { useContext, useState, type ComponentType } from "react";
import { Appearance } 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,
StyledConfiguration,
@@ -114,7 +114,7 @@ export function useNativeVariable(name: string) {
/**
* @deprecated Use `` instead.
*/
-export function vars(variables: Record) {
+export function vars(variables: Record) {
return Object.assign(
{ [VAR_SYMBOL]: "inline" },
Object.fromEntries(
diff --git a/src/runtime.types.ts b/src/runtime.types.ts
index 6e8f9ad8..f5f60bf5 100644
--- a/src/runtime.types.ts
+++ b/src/runtime.types.ts
@@ -136,6 +136,33 @@ export type InlineStyle =
| (Record | undefined | null)[]
| (() => unknown);
+/******************************* Variables ******************************/
+
+/**
+ * A value a CSS custom property can be given from JavaScript, via `vars()` or
+ * ``.
+ *
+ * Both platforms are written against this one type. A custom property holds a
+ * token stream on web and a structured value on native, so the type is the set
+ * of values both can honour:
+ *
+ * - An array is a comma-separated CSS list — a `font-family` stack, a
+ * `transition-property` list. A value whose parts are separated by spaces (a
+ * `box-shadow`, a `transform`) is a single string.
+ * - `undefined` leaves the property unset, so an ancestor's value inherits.
+ *
+ * A `StyleDescriptor` is wider than this: it also covers the `StyleFunction`
+ * tuples the compiler emits for `var()`, `rgba()` and friends. Those are an
+ * internal encoding of the native runtime and have no web serialisation, so
+ * they are not part of the public API.
+ */
+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 e43f800a..0c6fe476 100644
--- a/src/web/api.tsx
+++ b/src/web/api.tsx
@@ -15,7 +15,7 @@ import type {
StyledProps,
} from "react-native-css";
-import type { ReactComponent } from "../runtime.types";
+import type { CustomPropertyValue, ReactComponent } from "../runtime.types";
import { assignStyle } from "./assign-style";
const defaultMapping: StyledConfiguration> = {
@@ -78,33 +78,57 @@ export const colorScheme: ColorScheme = {
};
/**
- * @deprecated Use `` instead.
+ * Serialises a custom property into the token stream CSS stores it as. An array
+ * is a comma-separated list; an `undefined` member drops out of that list, and
+ * an `undefined` value leaves the property unset altogether.
*/
-export function vars(variables: Record) {
- const $variables: Record = {};
+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]);