Closed olivierpascal closed 10 months ago
You can have a look at the Next.js example to see how it might be done. But I don't think it works like you want to make it work in styleX.
I don't know if this is your question or not, but try to be explicit about what you want support for. Or describe the solution you're looking for.
Of course: for the needs of my app, i need to be able to demo a component in both light and dark mode on the same screen.
<div data-mode="light">
<MyStyledComponent className="myComponent" />
</div>
<div data-mode="dark">
<MyStyledComponent className="myComponent" />
</div>
I cannot use @media (prefers-color-scheme: light/dark)
because this is OS specific (unaffected by the color-scheme
css property).
I would like to be able to generate a style like:
:root {
--primary: red;
}
:is([data-mode="dark"]) {
--primary: blue;
}
.myComponent {
color: var(--primary);
}
Maybe from something like:
// tokens.stylex.js
import * as stylex from '@stylexjs/stylex';
export const colors = stylex.defineVars({
primary: {
default: 'red',
':is[dark-mode="dark"]': 'blue',
},
});
// MyComponent.jsx
import { colors } from './tokens.stylex';
const styles = stylex.create({
myComponent: {
color: colors.primary,
},
});
// ...
What do you think?
Or, to generate a style like:
:root {
--primary: red;
--primary-dark: blue;
}
.myComponent {
color: var(--primary);
}
:is([data-mode="dark"]) .myComponent {
color: var(--primary-dark);
}
Maybe from something like:
// tokens.stylex.js
import * as stylex from '@stylexjs/stylex';
export const colors = stylex.defineVars({
primary: 'red',
'primary-dark': 'blue',
});
// MyComponent.jsx
import { colors } from './tokens.stylex';
const styles = stylex.create({
myComponent: {
color: {
default: colors.primary,
':is[dark-mode="dark"]': colors['primary-dark'],
}
},
});
// ...
I know this syntax would conflict with pseudo classes, but you get the idea.
hey @olivierpascal how about using the required component as nested object psuedoSelector(dark).myComponent : colorValue psuedoSelector(light).myComponent : colorValue
for this there is a reference on docs, see if it helps defining styles
correct me if it doesn't relate to your usecase
Mmmh, the problem is that the pseudo selector :is([dark-mode="dark"])
should be at the root level and hence placed before the class name, like :is([data-mode="dark"]) .myComponent {}
or :is([data-mode="dark"] .myComponent) {}
.
I don't think it's possible to do so with a nested object, at the component level.
I think for my use case, the best API would be something like:
// tokens.stylex.js
import * as stylex from '@stylexjs/stylex';
export const colors = stylex.defineVars({
primary: {
default: 'red',
':is[dark-mode="dark"]': 'blue',
},
secondary: {
default: 'green',
':is[dark-mode="dark"]': 'black',
},
});
-or-
// tokens.stylex.js
import * as stylex from '@stylexjs/stylex';
export const colors = stylex.defineVars({
default: {
primary: 'red',
secondary: 'green',
},
':is[dark-mode="dark"]': {
primary: 'blue',
secondary: 'dark',
},
});
to generate:
/* stylex.css */
:root {
--primary: red;
--secondary: green;
}
:is([data-mode="dark"]) {
--primary: blue;
--secondary: black;
}
The remaining question would still be how to do the same at the component level, without the theming API.
Sorry to spam. I just see that [https://stylexjs.com]() stylesheet is compiling to this:
:root {
--ifm-color-primary: #872291;
/* ... */
}
html[data-theme=dark]:root {
--ifm-color-primary: #be51c8;
/* ... */
}
I don't know if it uses stylex or pure css. Probably a mix of both.
There's a lot here already, so I'll try to just answer in the initial question. Using dark mode based on a user controlled data-id
. This can be done by creating default variables with defineVars
and then creating separate themes with createTheme
.
Something like:
// vars.stylex.js
import * as stylex from '@stylexjs/stylex';
export const colors = stylex.defineVars({
primary: 'red',
// ...
});
// color-themes.js
import * as stylex from '@stylexjs/stylex';
import {colors} from './vars.stylex.js';
export const darkTheme = stylex.createTheme(colors, {
primary: 'blue',
// ...
});
// layout.tsx
import {darkTheme} from './color-themes';
export default function RootLayout({isDarkMode, children}) {
return (
<html {...stylex.props(isDarkMode && darkTheme)}>
<head>{/* ... */}</head>
<body>
{children}
</body>
</html>
);
}
Now you can just import and use the variables from vars.stylex.ts
directly
and use them. They will resolve to light mode or not based on whether
the theme is set on the html
tag or not.
Things to note:
data-id
for StyleX, but you can keep it if you want it for other things.layout.tsx
is an example for how this might be done in NextJS. You can set the theme
on the html
element any way you want. e.g. If you're using DOM APIs for this, that will work too.defineVars
export. If you want a "light" theme, a "dark" theme, and a default theme that uses @media
to change automatically you can do that too. Change the original variables defined with defineVars
to use media queries, then create two themes for light and dark that don't use media queries.
Just to add on to the conversation, you can also use a Context (if you are using React or Nextjs) to switch between themes globally similar to most other styling libraries out there
const ThemeContext = React.createContext(undefined)
...
const [theme, setTheme] = useState('light');
...
// You can also append props to children directly through cloneElement (but is discouraged by React team)
<ThemeContext.Provider value={ theme }>
<body {...props(theme === 'dark' && darkTheme)}>
{children}
</body>
</ThemeContext.Provider>
You can also use custom hooks and an effect to automatically set default theme and persist data to local storage.
React docs: https://react.dev/reference/react/createContext shows the same exact application method.
This way you would not have to create one defineVars
and two separate themes
to use it with @media
, you can just create the same defineVars with an added theme as mentioned above by @nmn, but this may not be a recommended use pattern (and why he did not cover it).
I didn't cover the JS bits but @kevintyj's suggestion is valid for React.
This way you would not have to create one defineVars and two separate themes to use it with @media
I'm not clear what this means so I'll try to clarify what I meant by multiple themes.
Use defineVars
to create the default light theme, use createTheme
to create a dark theme. Apply dark
theme conditionally as suggested by @kevintyj
Use defineVars
and use the media query to make this theme adapt to the media query. Use createTheme
to create the "light" and "dark" themes that always use the light or dark colors respectively.
@nmn your solution works like a charm, thank you. I have nothing to add, maybe we can close this topic? Thanks all very much.
@nmn If we possess numerous properties but wish to override certain values, what would be the most effective method to accomplish this? I used ...colors.__tokens
but it didn't work, so I have to export base values.
Here is the code I've written, but I think it's not quite right.
// tokens.stylex.tsx
import * as stylex from '@stylexjs/stylex';
const DARK = '@media (prefers-color-scheme: dark)';
export const colorsRepo = {
primaryText: { default: 'black', [DARK]: 'white' },
secondaryText: { default: '#333', [DARK]: '#ccc' },
accent: { default: 'blue', [DARK]: 'lightblue' },
background: { default: 'white', [DARK]: 'black' },
lineColor: { default: 'gray', [DARK]: 'lightgray' },
borderRadius: '4px',
fontFamily: 'system-ui, sans-serif',
fontSize: '16px',
};
export const colors = stylex.defineVars(colorsRepo);
export const spacing = stylex.defineVars({
none: '0px',
xsmall: '4px',
small: '8px',
medium: '12px',
large: '20px',
xlarge: '32px',
xxlarge: '48px',
xxxlarge: '96px',
});
//themes.ts
import * as stylex from '@stylexjs/stylex';
import { colors, colorsRepo } from './tokens.stylex';
// A constant can be used to avoid repeating the media query
const DARK = '@media (prefers-color-scheme: dark)';
// Dracula theme
export const dracula = stylex.createTheme(colors, {
...colorsRepo,
primaryText: { default: 'purple', [DARK]: 'lightpurple' },
background: { default: '#555', [DARK]: 'black' },
});
// MyComponent.tsx
'use client';
import * as stylex from '@stylexjs/stylex';
import { colors, spacing } from './tokens.stylex';
import { dracula } from './themes';
import { useState } from 'react';
const styles = stylex.create({
container: {
color: colors.primaryText,
backgroundColor: colors.background,
padding: spacing.medium,
marginTop: '10px',
fontSize: colors.fontSize,
},
});
export const MyComponent = ({ children }: any) => {
const [isDark, setDark] = useState(false);
const toggle = () => {
setDark(!isDark);
};
return (
<div {...stylex.props(isDark && dracula)}>
<div {...stylex.props(styles.container)}>
<button onClick={toggle}>Toggle</button>
{children}
</div>
</div>
);
};
And one more thing, the class for debug mode is
UnknownFile__dracula
I test with https://github.com/facebook/stylex/tree/main/apps/nextjs-example)
<div class="x1wm1iiy UnknownFile__dracula">
<button>Toggle</button>
<div class="MyComponent__styles.container x13ca1vq x1lfge5 x1nq940n xf0e2mi">Hello
</div>
</div>
I had this bug the very first time I used stylex (with some code straight from the doc) but, as I stopped using media queries, I forgot to create an issue.
On my side, the following does work:
export const colorsRepo = {
background: 'red',
};
export const colorsRepo = {
background: {
default: 'red', // work
whatever: 'lightgray',
},
};
When the following silently fail (the entire var group is not set):
export const colorsRepo = {
background: {
default: 'red',
'@media (prefers-color-scheme: dark)': 'lightgray',
},
};
If you can reproduce this bug, maybe create a new issue with a minimal reproducible code?
If we possess numerous properties but wish to override certain values, what would be the most effective method to accomplish this?
This was a type constraint that I fixed yesterday. In the next version, you'll be able to override only some of the variables when creating a theme.
@olivierpascal Could you give me a slightly more detailed example?
Ok @nmn, here is a full reproducible example:
// tokens.stylex.ts
import * as stylex from '@stylexjs/stylex';
// A constant can be used to avoid repeating the media query
const DARK = '@media (prefers-color-scheme: dark)';
export const colors = stylex.defineVars({
primary: { default: 'blue', [DARK]: 'green' }, // NOT OK: text output is black
// primary: 'blue', // OK: text output is blue
// primary: { default: 'blue', whatever: 'green' }, // OK: text output is blue
});
// MyComponent.tsx
import * as stylex from '@stylexjs/stylex';
import React from 'react';
import { colors } from './tokens.stylex';
const styles = stylex.create({
container: {
color: colors.primary,
},
});
export const MyComponent: React.FC = () => (
<div {...stylex.props(styles.container)}>TEXT</div>
);
The CSS var is not compiled.
Probably related to https://github.com/facebook/stylex/issues/235
@olivierpascal Yup, I think this is caused by #235 as well. Can you try a production build to verify in your case as well?
v0.4.1 Fixes the issues mentioned.
for me this is working both with context and prefers-color-scheme.
//themes.ts
const DARK = '@media (prefers-color-scheme: dark)';
export const lightTheme = stylex.createTheme(colors, {
primary: 'blue',
bg: {
default: "white",
[DARK]: "white",
}
})
export const darkTheme = stylex.createTheme(colors, {
primary: 'blue',
bg: {
default: "black",
[DARK]: "black",
}
})
how can i make it work without the colors object, If possible. I couldn't get the red or blue to appear both changing the prefers-color-scheme though the browser and by changing the theme via context.
export const colors = stylex.defineVars({
primary: 'red',
bg: {
default: "red",
[DARK]: "blue",
}
});
then i made this provider like suggested above:
//providers.tsx
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
setTheme: Dispatch<SetStateAction<Theme>>
}
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export const ThemeProvider = ({ children }: Props) => {
const [theme, setTheme] = useState<Theme>('dark')
const contextValue: ThemeContextType = { theme, setTheme }
return (
<ThemeContext.Provider value={contextValue}>
<html {...stylex.props(s.html, s.reset)} lang="en">
<body {...stylex.props(s.reset, s.body, theme === 'dark' ? darkTheme : lightTheme)}>
{children}
</body>
</html>
</ThemeContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
//component.ts
const { setTheme } = useTheme();
//layout.ts
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
{children}
</ThemeProvider>
)
}
how can i make it work without the colors object, If possible
@alejandroyunes I don't understand what you're asking.
hi, yes I think what I was asking is that if primary isnt any of the createThemplate it takes what is in the colors object in defineVars,
//themes.ts
const DARK = '@media (prefers-color-scheme: dark)';
export const lightTheme = stylex.createTheme(colors, {
bg: {
default: "white",
[DARK]: "white",
}
})
export const darkTheme = stylex.createTheme(colors, {
bg: {
default: "black",
[DARK]: "black",
}
})
export const colors = stylex.defineVars({
primary: 'red',
bg: {
default: "red",
[DARK]: "blue",
}
});
why do I need the bg in colors and in both themes?
I still don't understand what you're asking, but you should be able to write this instead:
//themes.ts
const DARK = '@media (prefers-color-scheme: dark)';
export const lightTheme = stylex.createTheme(colors, {
bg: "white",
})
export const darkTheme = stylex.createTheme(colors, {
bg: "black",
})
export const colors = stylex.defineVars({
primary: 'red',
bg: {
default: "red",
[DARK]: "blue",
}
});
why do I need the bg in colors and in both themes?
You don't...
Sorry to bring up an old thread but I'm not able to do dark mode without FOUCs.
Given this example...
// layout.tsx
import {darkTheme} from './color-themes';
export default function RootLayout({isDarkMode, children}) {
return (
<html {...stylex.props(isDarkMode && darkTheme)}>
<head>{/* ... */}</head>
<body>
{children}
</body>
</html>
);
}
I'm using Astro and during server-side rendering, dark mode is unknown so it defaults to light mode. Now on client hydration we get the light mode rendered, then once the React code kicks in and recognizes that dark mode is set, it re-renders.
The way I got this working in other styling libraries is by using descendent selectors similar to html[data-theme='dark'] .test
. Even on the first render from SSR, the styles are correct. Is there an escape hatch here?
Thanks so much @nmn!
I'm using Astro and during server-side rendering, dark mode is unknown so it defaults to light mode.
This is the problem here. We have media queries for detecting dark mode. You shouldn't be trying to choose light mode or dark mode on the server at all.
I'm working on adding first-class support for light-dark()
. Which will make doing this even more flexible.
But TLDR; Setting themes based on selectors such as html[data-theme='dark']
should be avoided as a best practice. But even if you're using them, they should only be used when the user has manually overridden their preferred color scheme which should be known on the server. If you don't know their preferred color scheme, you should be using Media Queries to choose automatically. (And use light-dark()
in the future)
Gotcha, so the rec is to store user prefs for dark / light on server and then use that for server side rendering.
FWIW, the stylex website uses html[data-theme=dark]
.
FWIW, the stylex website uses html[data-theme=dark].
The only SSR-safe ways of doing color theming is:
Docusaurus generates a static bundle so there is no server to store user preferences on.
:(
Is your feature request related to a problem? Please describe.
My app set dark mode with
<html data-mode="dark">
. I don't think there is a way to use stylex to generate a css style like:is([data-mode="dark"] .myclass) { ... }
.Describe a solution you'd like Describe alternatives you've considered Additional context