-
Instead of using global theming, I've been thinking about having components defining their own abstract theme tokens. This way I don't need to worry about components being tightly coupled to application-level theming tokens, and there are default values for everything even if I don't set any. This is what I came up with: import * as stylex from "@stylexjs/stylex";
import * as React from "react";
import { Center } from "~/components/center";
import { Row } from "~/components/flexbox/flexbox";
import { LucideIcons } from "~/components/lucide-icons";
import { Sx } from "~/styles/sx";
import { Spacing, Theme } from "~/styles/theme.stylex";
const CHECKBOX_BORDER_RADIUS = "--checkbox-border-radius";
const CHECKBOX_COLOR = "--checkbox-color";
const CHECKBOX_SIZE = "--checkbox-size";
const CHECKBOX_ICON_SIZE = "--checkbox-icon-size";
const defaults = {
[CHECKBOX_BORDER_RADIUS]: "37.5%",
[CHECKBOX_COLOR]: Theme.Brand,
[CHECKBOX_SIZE]: "48px",
[CHECKBOX_ICON_SIZE]: `calc(var(${CHECKBOX_SIZE}) / 2)`,
};
const vars = {
CHECKBOX_BORDER_RADIUS: `var(${CHECKBOX_BORDER_RADIUS}, ${defaults[CHECKBOX_BORDER_RADIUS]})`,
CHECKBOX_COLOR: `var(${CHECKBOX_COLOR}, ${defaults[CHECKBOX_COLOR]})`,
CHECKBOX_SIZE: `var(${CHECKBOX_SIZE}, ${defaults[CHECKBOX_SIZE]})`,
CHECKBOX_ICON_SIZE: `var(${CHECKBOX_ICON_SIZE}, ${defaults[CHECKBOX_ICON_SIZE]})`,
};
const styles = stylex.create({
intrinsic: {
blockSize: vars.CHECKBOX_SIZE,
inlineSize: vars.CHECKBOX_SIZE,
},
// This is the background layer
background: {
":focus-within::before": {
borderRadius: "inherit",
boxShadow: `0 0 0 2px ${vars.CHECKBOX_COLOR}`,
content: "",
inset: -2,
pointerEvents: "none", // Ensure that this layer doesn't receive pointer events
position: "absolute",
},
alignItems: "center",
blockSize: vars.CHECKBOX_SIZE,
borderRadius: vars.CHECKBOX_BORDER_RADIUS,
display: "flex",
inlineSize: vars.CHECKBOX_SIZE,
justifyContent: "center",
position: "relative", // For icon
},
unchecked: {
backgroundColor: null,
boxShadow: `inset 0 0 0 1px color-mix(in hsl, transparent, white 50%)`,
},
checked: {
backgroundColor: vars.CHECKBOX_COLOR,
boxShadow: `inset 0 0 0 1px color-mix(in hsl, transparent, white 50%)`,
//// boxShadow: null,
},
// This is the foreground layer
foreground: {
blockSize: vars.CHECKBOX_ICON_SIZE,
color: "white",
inlineSize: vars.CHECKBOX_ICON_SIZE,
inset: 0,
margin: "auto", // Self-center
pointerEvents: "none", // Ensure that this layer doesn't receive pointer events
position: "absolute",
},
});
export type CheckboxProps = Sx.IntrinsicComponentProps<"input"> & {
// Require the checked state and the onChange event handler
intrinsicProps: {
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
};
function Checkbox(props: CheckboxProps): React.JSX.Element {
return (
<div
{...stylex.props(
styles.background,
(() => {
if (props.intrinsicProps.checked) {
return styles.checked;
} else {
return styles.unchecked;
}
})(),
props.stylex,
)}
>
{/* This is the native input */}
<input {...props.intrinsicProps} {...stylex.props(styles.intrinsic)} type="checkbox" />
{props.intrinsicProps.checked && <LucideIcons.Check {...stylex.props(styles.foreground)} strokeWidth={4} />}
</div>
);
}
const custom = stylex.create({
red: {
[CHECKBOX_COLOR]: Theme.Red,
},
big: {
[CHECKBOX_ICON_SIZE]: `calc(var(${CHECKBOX_SIZE}) / 2)`,
[CHECKBOX_SIZE]: "64px",
},
});
type UseCheckbox = {
checked: boolean;
setChecked: React.Dispatch<React.SetStateAction<boolean>>;
};
function useCheckbox(initialState: boolean): UseCheckbox {
const [checked, setChecked] = React.useState(initialState);
return { checked, setChecked };
}
export function UsageCheckbox(): React.JSX.Element {
//// const checkboxes = [useCheckbox(false), useCheckbox(false), useCheckbox(false)];
const checkbox1 = useCheckbox(false);
const checkbox2 = useCheckbox(false);
const checkbox3 = useCheckbox(false);
return (
<Center withMinBlockSize>
<Row alignItems="center" gap={Spacing.Md}>
<Checkbox
//// stylex={custom.checkbox as stylex.StyleXStyles}
intrinsicProps={{
checked: checkbox1.checked,
onChange: (e) => checkbox1.setChecked(e.target.checked),
}}
/>
<Checkbox
stylex={custom.big as stylex.StyleXStyles}
intrinsicProps={{
checked: checkbox2.checked,
onChange: (e) => checkbox2.setChecked(e.target.checked),
}}
/>
<Checkbox
stylex={custom.red as stylex.StyleXStyles}
intrinsicProps={{
checked: checkbox3.checked,
onChange: (e) => checkbox3.setChecked(e.target.checked),
}}
/>
</Row>
</Center>
);
} So if you impose the constraint of not wanting to create a +1 file per component for coordinating themes, is there some other way to do this or is this reasonable? What I like about this is that I can still export the theme tokens and theoretically override them from another file. Here's how this looks in practice: icon.mp4 |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 6 replies
-
Ah unfortunately it doesn't work if I separate |
Beta Was this translation helpful? Give feedback.
-
OK I think I see the light. This is likely the canoncial way to solve this problem, it simply requires creating a // checkbox.stylex.ts
import * as stylex from "@stylexjs/stylex";
import { Theme } from "~/styles/theme.stylex";
export const CheckboxVars = stylex.defineVars({
CheckboxBorderRadius: "37.5%",
CheckboxColor: Theme.Brand,
CheckboxSize: "48px",
CheckboxIconSize: "50%",
}); // checkbox.tsx
import * as stylex from "@stylexjs/stylex";
import * as React from "react";
import { CheckboxVars } from "~/components/checkbox.stylex";
import { LucideIcons } from "~/components/lucide-icons";
import { Sx } from "~/styles/sx";
const styles = stylex.create({
intrinsic: {
blockSize: CheckboxVars.CheckboxSize,
inlineSize: CheckboxVars.CheckboxSize,
},
// This is the background layer
background: {
":focus-within::before": {
borderRadius: "inherit",
boxShadow: `0 0 0 2px ${CheckboxVars.CheckboxColor}`,
content: "",
inset: -2,
pointerEvents: "none", // Ensure that this layer doesn't receive pointer events
position: "absolute",
},
alignItems: "center",
blockSize: CheckboxVars.CheckboxSize,
borderRadius: CheckboxVars.CheckboxBorderRadius,
display: "flex",
inlineSize: CheckboxVars.CheckboxSize,
justifyContent: "center",
position: "relative", // For icon
},
unchecked: {
backgroundColor: null,
boxShadow: `inset 0 0 0 1px color-mix(in hsl, transparent, white 50%)`,
},
checked: {
backgroundColor: CheckboxVars.CheckboxColor,
boxShadow: `inset 0 0 0 1px color-mix(in hsl, transparent, white 50%)`,
//// boxShadow: null,
},
// This is the foreground layer
foreground: {
blockSize: CheckboxVars.CheckboxIconSize,
color: "white",
inlineSize: CheckboxVars.CheckboxIconSize,
inset: 0,
margin: "auto", // Self-center
pointerEvents: "none", // Ensure that this layer doesn't receive pointer events
position: "absolute",
},
});
export type CheckboxProps = Sx.IntrinsicComponentProps<"input"> & {
// Require the checked state and the onChange event handler
intrinsicProps: {
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
};
export function Checkbox(props: CheckboxProps): React.JSX.Element {
return (
<div
{...stylex.props(
styles.background,
(() => {
if (props.intrinsicProps.checked) {
return styles.checked;
} else {
return styles.unchecked;
}
})(),
props.stylex,
)}
>
{/* This is the native input */}
<input {...props.intrinsicProps} {...stylex.props(styles.intrinsic)} type="checkbox" />
{props.intrinsicProps.checked && <LucideIcons.Check {...stylex.props(styles.foreground)} strokeWidth={4} />}
</div>
);
} // idea.tsx
import * as stylex from "@stylexjs/stylex";
import * as React from "react";
import { Center } from "~/components/center";
import { Checkbox } from "~/components/checkbox";
import { CheckboxVars } from "~/components/checkbox.stylex";
import { Row } from "~/components/flexbox/flexbox";
import { Spacing, Theme } from "~/styles/theme.stylex";
const red = stylex.createTheme(CheckboxVars, {
CheckboxColor: Theme.Red,
});
const big = stylex.createTheme(CheckboxVars, {
CheckboxSize: "64px",
});
type UseCheckbox = {
checked: boolean;
setChecked: React.Dispatch<React.SetStateAction<boolean>>;
};
function useCheckbox(initialState: boolean): UseCheckbox {
const [checked, setChecked] = React.useState(initialState);
return { checked, setChecked };
}
export function UsageCheckbox(): React.JSX.Element {
//// const checkboxes = [useCheckbox(false), useCheckbox(false), useCheckbox(false)];
const checkbox1 = useCheckbox(false);
const checkbox2 = useCheckbox(false);
const checkbox3 = useCheckbox(false);
return (
<Center withMinBlockSize>
<Row alignItems="center" gap={Spacing.Md}>
<Checkbox
intrinsicProps={{
checked: checkbox1.checked,
onChange: (e) => checkbox1.setChecked(e.target.checked),
}}
/>
<Checkbox
stylex={big}
intrinsicProps={{
checked: checkbox2.checked,
onChange: (e) => checkbox2.setChecked(e.target.checked),
}}
/>
<Checkbox
stylex={red}
intrinsicProps={{
checked: checkbox3.checked,
onChange: (e) => checkbox3.setChecked(e.target.checked),
}}
/>
</Row>
</Center>
);
} |
Beta Was this translation helpful? Give feedback.
-
FWIW, it will be easier for us to understand topics of discussion in the future if you make example code simpler and limited to the topic at hand.
Avoiding the coupling to app tokens is a requirement for many component libraries! This is something that you can do in StyleX by defining the variables in a separate file, as you did in your later code snippets. A top-level theme can then import all the CSS var fragments from components and set new values for them - the theme style can then be set on the app's root element and it will restyle all the components.
Whether
This isn't a solution for re-theming though - it only restyles instances, and creates inline styles. |
Beta Was this translation helpful? Give feedback.
TLDR; This is what I was going to suggest as the canonical way to do this pattern.
In the future, we might let you use
stylex.defineVars
in any file so you wouldn't need to put the variables in a separate file, but there are very good reasons to not allow that for now. (co-location of variable defs would make compilation and caching a lot harder)However, I would suggest against this pattern in general. This is a pattern we tried as well and it leads to a couple of problems:
It causes CSS bloat.
The benefit of atomic CSS is that common styles can be shared across your entire app. If every component defines its own variables this becomes impossible and you CSS will start to grow linearly …