Use props inside emotion's css string template - css

I'm in the middle of migrating my design system from styled-components to emotion.
In SC, The following syntax is considered valid:
export interface AvatarProps {
// ...
shape: AvatarShape;
size: AvatarSize;
}
const borderRadiusByShape = css<Pick<AvatarProps, "shape" | "size">>`
border-radius: ${(props) => {
return match(props.shape)
.with("circle", () => `${props.size}px`)
.with("square", () => "0px")
...
.exhaustive();
}}
`;
const StyledAvatar = styled.div<AvatarProps>`
/* ... */
${borderRadiusByShape};
`;
This pattern allows me to reuse my borderRadius across multiple styled.[x] declarations.
Looking at emotion's css function, it looks like emotion doesn't support that syntax. Is there any workaround that won't require me to wrap this functionality with a new component?

Afer a bit of digging, it turns out I can extract the props from css and create a function that accept these props and return the corresponding CSS:
export interface AvatarProps {
// ...
shape: AvatarShape;
size: AvatarSize;
}
const borderRadiusByShape = (props: Pick<AvatarProps, "shape" | "size">) => css`
border-radius: ${match(props.shape)
.with("circle", () => `${props.size}px`)
.with("square", () => "0px")
/* ... */
.exhaustive()};
`;
const StyledAvatar = styled.div<AvatarProps>`
/* ... */
${borderRadiusByShape};
`;

Related

Change css-module dynamically on React

I have a canvas like this in Component
It can change the pointer to crosshair when cursor is entered on Canvas.
import styles from '../css/basic-styles.module.css';
const ImagePreview = () =>{
[mode,setMode] = useState(0);
changeMode(mode){
setMode(mode);
}
return (){
<canvas className={styles.canvas} width=200 height=200></canvas>
}
}
in css
canvas:hover{
/*cursor:pointer;*/
cursor:crosshair;
}
Now I want to change the css dynamically depending on the mode value.
I want to use pointer when mode is 1
I should quite use css? or is there any method to make it work?
Please see the solution below. It is also available in the sandbox.. Ignore the vanilla react solution it included for the snippet runner. Click Run Code Snippet to see preview here.
/* Solution
const ImagePreview = () => {
const [mode, setMode] = useState(0);
return (
<canvas
onMouseEnter={() => setMode(1)}
onMouseLeave={() => setMode(0)}
style={{
backgroundColor: "teal",
cursor: mode ? "crosshair" : "pointer"
}}
width={200}
height={200}
/>
);
};
*/
// This is so it can work in Stackoverflow snippet preview. //
const ImagePreview = () => {
const [mode, setMode] = React.useState(0);
const canvasCfg = {
onMouseEnter: () => setMode(1),
onMouseLeave: () => setMode(0),
style: { backgroundColor: "teal", cursor: mode ? "crosshair" : "pointer"},
width: 200,
height: 200
}
return React.createElement('canvas', canvasCfg)
}
const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(React.createElement(ImagePreview));
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
create two classes like this
canvas1:hover{
cursor:pointer;
}
canvas2:hover{
cursor:crosshair;
}
and use it conditionally based on mode like
import styles from '../css/basic-styles.module.css';
const ImagePreview = () =>{
[mode,setMode] = useState(0);
changeMode(mode){
setMode(mode);
}
return (){
<canvas className={mode == 1 ? styles.canvas1 : styles.canvas2} width=200 height=200></canvas>
}
}
you can also use classes without :hover , this will also give the desired result because of cursor property
.canvas1{
cursor: pointer;
}
.canvas2{
cursor: crosshair;
}

Get value of CSS rule from CSS module

Let's say I have a React component with a simple CSS module:
/* MyComponent.module.css */
.animatable {
background-color: red;
transition-property: background-color;
transition-duration: 500ms;
}
.animating {
background-color: grey;
}
// MyComponent.js
import styles from "./MyComponent.module.css"
const MyComponent = () => {
const [isAnimating, setIsAnimating] = useState(false)
const classes = `${styles.animatable} ${isAnimating? styles.animating : ''}`
const clickHandler = () => {
setIsAnimating(true)
setTimeout(() => setIsAnimating(false), 500)
}
return <div className={classes} onClick={clickHandler}>Click me.</div>
}
export default MyComponent
There is a little code duplication in that the CSS transition-duration and the setTimeout() to revert the animation both hard-code the duration.
I think it would make sense to have a single source of truth and that it can be the CSS, since it's the "most static" of the two files. From the js file I have access to styles from the CSS module, but is there a way to fetch that transition-duration: 500ms so that I can reuse it?
One option would be to listen for the transitionend event instead of using setTimeout:
const MyComponent = () => {
const [isAnimating, setIsAnimating] = useState(false)
const ref = useRef()
const classes = `animatable ${isAnimating ? 'animating' : ''}`
const clickHandler = () => {
setIsAnimating(true)
}
useEffect(() => {
if (!isAnimating) return
const transitionendHandler = () => {
setIsAnimating(false)
}
ref.current.addEventListener('transitionend', transitionendHandler)
return () => {
ref.current.removeEventListener('transitionend', transitionendHandler)
}
}, [isAnimating])
return (
<div
ref={ref}
className={classes}
onClick={clickHandler}
>
Click me.
</div>
)
}

How to override classes using makeStyles and useStyles in material-ui?

Consider a component that renders a button and says this button should have a red background and a yellow text color. Also there exists a Parent component that uses this child but says, the yellow color is fine, but I want the background color to be green.
withStyles
No problem using the old withStyles.
import React from "react";
import { withStyles } from "#material-ui/core/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
const ChildWithStyles = withStyles(childStyles)(({ classes }) => {
return <Button classes={classes}>Button in Child withStyles</Button>;
});
const ParentWithStyles = withStyles(parentStyles)(({ classes }) => {
return <ChildWithStyles classes={classes} />;
});
export default ParentWithStyles;
https://codesandbox.io/s/passing-classes-using-withstyles-w17xs?file=/demo.tsx
makeStyles/useStyles
Let's try the makeStyles/useStyles instead and follow the guide Overriding styles - classes prop on material-ui.com.
import React from "react";
import { makeStyles } from "#material-ui/styles";
import { Button } from "#material-ui/core";
const parentStyles = {
root: {
background: "green"
}
};
const childStyles = {
root: {
background: "red"
},
label: {
color: "yellow"
}
};
// useStyles variant does NOT let me override classes
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
const ChildUseStyles = ({ classes: classesOverride }) => {
const classes = useChildStyles({ classes: classesOverride });
return (
<>
<Button classes={classes}>Button1 in Child useStyles</Button>
<Button classes={classesOverride}>Button2 in Child useStyles</Button>
</>
);
};
const AnotherChildUseStyles = props => {
const classes = useChildStyles(props);
return (
<>
<Button classes={classes}>Button3 in Child useStyles</Button>
</>
);
};
const ParentUseStyles = () => {
const classes = useParentStyles();
return <>
<ChildUseStyles classes={classes} />
<AnotherChildUseStyles classes={classes} />
</>
};
export default ParentUseStyles;
https://codesandbox.io/s/passing-classes-using-usestyles-6x5hf?file=/demo.tsx
There seems no way to get the desired effect that I got using withStyles. A few questions, considering I still want the same effect (green button yellow text) using some method of classes overriding (which seemed to make sense to me before).
How is my understanding wrong about how to pass classes as means to override parts of them using useStyles?
How should I approach it alternatively?
And if I'm using the wrong approach, why is material-ui still giving me a warning when the parent has something in the styles that the child doesn't have?
the key something provided to the classes prop is not implemented in [Child]
Is the migration from the old approach (withStyles) vs the new approach documented somewhere?
Btw, I'm aware of this solution but that seems cumbersome when you have too much you want to override.
const useStyles = makeStyles({
root: {
backgroundColor: 'red',
color: props => props.color, // <-- this
},
});
function MyComponent(props) {
const classes = useStyles(props);
return <div className={classes.root} />;
}
withStyles has very little functionality in it. It is almost solely a wrapper to provide an HOC interface to makeStyles / useStyles. So all of the functionality from withStyles is still available with makeStyles.
The reason you aren't getting the desired effect is simply because of order of execution.
Instead of:
const useParentStyles = makeStyles(parentStyles);
const useChildStyles = makeStyles(childStyles);
you should have:
const useChildStyles = makeStyles(childStyles);
const useParentStyles = makeStyles(parentStyles);
The order in which makeStyles is called determines the order of the corresponding style sheets in the <head> and when specificity is otherwise the same, that order determines which styles win (later styles win over earlier styles). It is harder to get that order wrong using withStyles since the wrapper that you are using to override something else will generally be defined after the thing it wraps. With multiple calls to makeStyles it is easier to do an arbitrary order that doesn't necessarily put the overrides after the base styles they should impact.
The key to understanding this is to recognize that you aren't really passing in overrides, but rather a set of classes to be merged with the new classes. If childClasses.root === 'child_root_1' and parentClasses.root === 'parent_root_1', then the merged result is mergedClasses.root === 'child_root_1 parent_root_1' meaning any elements that have their className set to mergedClasses.root are receiving both CSS classes. The end result (as far as what overrides what) is fully determined by CSS specificity of the styles in the two classes.
Related answers:
Material UI v4 makeStyles exported from a single file doesn't retain the styles on refresh
Internal implementation of "makeStyles" in React Material-UI?
In Material-ui 4.11.x while creating styles using makeStyles wrap the enclosing styles with createStyles, and this style will have highest priority than the default one.
const useStyles = makeStyles((theme: Theme) =>
createStyles({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
}),
);
You could try removing the createStyles and see the difference.
code source from https://material-ui.com/components/backdrop/
One way to achieve this using withStyles is the following and can be helpful to override css classes.
Supposing that you want to override a class called ".myclass" which contains "position: absolute;":
import { withStyles } from '#material-ui/styles';
const styles = {
"#global": {
".myClass": {
position: "relative",
}
}
};
const TestComponent = (props) => (
<>
<SomeComponent {...props}>
</>
);
export default withStyles(styles)(TestComponent);
After doing this, you override the definition of .myClass defined on <SomeComponent/> to be "position: relative;".

How to switch between themes in Ant design v4 dynamically?

I'd like to implement switching between dark/light theme dynamically with Ant design v4.
It's possible to customize the theme with other CSS/LESS imports as it's written here:
https://ant.design/docs/react/customize-theme#Use-dark-theme
But I'm not sure how to switch between those themes dynamically from the code. I have a variable in my React app (darkMode) which indicates if the dark theme is currently used. I have to provide correct CSS files when this variable is changed. But I can't import CSS dynamically only when some condition is fulfilled, because it's not way how the imports work.
I tried to do something messy with require like in the following code, but it's a very very bad approach and it's still not working properly (because CSS is injected but probably not withdrawn.
):
const Layout = () => {
...
useEffect(() => {
if (darkMode === true) {
require("./App.dark.css")
} else {
require("./App.css")
}
}, [darkMode])
return (
<Home />
)
}
It should be possible to switch themes somehow because it's already implemented in Ant design docs (https://ant.design/components/button/):
Do you have any idea how to do it?
Thanks!
This is what I am using for now -
PS -
I don't know if this will yield optimal bundle size.
changing theme results in a page reload.
make a folder called "themes" - it would have 6 files -> dark-theme.css, dark-theme.jsx, light-theme.css, light-theme.jsx, use-theme.js, theme-provider.jsx. Each of them is described below.
dark-theme.css
import "~antd/dist/antd.dark.css";
dark-theme.jsx
import "./dark-theme.css";
const DarkTheme = () => <></>;
export default DarkTheme;
light-theme.css
#import "~antd/dist/antd.css";
light-theme.jsx
import "./light-theme.css";
const LightTheme = () => <></>;
export default LightTheme;
use-theme.js A custom hook that different components can use -
import { useEffect, useState } from "react";
const DARK_MODE = "dark-mode";
const getDarkMode = () => JSON.parse(localStorage.getItem(DARK_MODE)) || false;
export const useTheme = () => {
const [darkMode, setDarkMode] = useState(getDarkMode);
useEffect(() => {
const initialValue = getDarkMode();
if (initialValue !== darkMode) {
localStorage.setItem(DARK_MODE, darkMode);
window.location.reload();
}
}, [darkMode]);
return [darkMode, setDarkMode];
};
theme-provider.jsx
import { lazy, Suspense } from "react";
import { useTheme } from "./use-theme";
const DarkTheme = lazy(() => import("./dark-theme"));
const LightTheme = lazy(() => import("./light-theme"));
export const ThemeProvider = ({ children }) => {
const [darkMode] = useTheme();
return (
<>
<Suspense fallback={<span />}>
{darkMode ? <DarkTheme /> : <LightTheme />}
</Suspense>
{children}
</>
);
};
change index.js to -
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
now, in my navbar suppose I have a switch to toggle the theme. This is what it would look like -
const [darkMode, setDarkMode] = useTheme();
<Switch checked={darkMode} onChange={setDarkMode} />
you must create 2 components
the first one :
import './App.dark.css'
const DarkApp =() =>{
//the app container
}
and the second :
import './App.light.css'
const LightApp =() =>{
//the app container
}
and create HOC to handle darkMode like this :
const AppLayout = () =>{
const [isDark , setIsDark] = useState(false);
return (
<>
{
isDark ?
<DarkApp /> :
<LightApp />
}
</>
)
}
ANTD internally uses CSS variables for the "variable.less" file. We can override these variables to make the default variables have dynamic values at runtime.
This is one way it can be achieved:
app-colors.less file:
:root{
--clr-one: #fff;
--clr-two: #000;
--clr-three: #eee;
}
// Dark theme colors
[data-thm="dark"]{
--clr-one: #f0f0f0;
--clr-two: #ff0000;
--clr-three: #fff;
}
We can now override the default ANTD variables in antd-overrides.less file:
#import <app-colors.less file-path>;
#primary-color: var(--clr-one);
#processing-color: var(--clr-two);
#body-background: var(--clr-three);
We usually import the antd.less file to get the antd styles, We would change that to "antd.variable.less"(to use css variables).
index.less file:
#import "~antd/dist/antd.variable.less";
#import <antd-overrides.less file-path>;
Now we need to toggle the "data-thm" attribute on a parent container(body tag recommended) to change the set of CSS variables that get used.
const onThemeToggle = (themeType) => {
const existingBodyAttribute = document.body.getAttribute("data-thm");
if (themeType === "dark" && existingBodyAttribute !== "dark") {
document.body.setAttribute("data-thm", "dark");
} else if (themeType === "light" && existingBodyAttribute) {
document.body.removeAttribute("data-thm");
}
};
The above piece of code can be called on a Theme toggle button or during component mount.
In Ant's example one suggestion is to import your "dark mode" CSS or LESS file into your main style sheet.
// inside App.css
#import '~antd/dist/antd.dark.css';
Instead of trying to toggle stylesheets, the "dark" styles are combined with base styles in one stylesheet. There are different ways to accomplish this, but the common pattern will be:
have a dark-mode selector of some sort in your CSS
put that selector in your HTML
have a way to toggle it on or off.
Here is a working example:
https://codesandbox.io/s/compassionate-elbakyan-f7tun?file=/src/App.js
In this example, toggling the state of darkMode will add or remove a dark-mode className to the top level container.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [darkMode, setDarkMode] = useState(false);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
<label>
<input
type="checkbox"
checked={darkMode}
onChange={() => setDarkMode((darkMode) => !darkMode)}
/>
Dark Mode?
</label>
<h1>Hello CodeSandbox</h1>
</div>
);
}
If darkMode is true, and the dark-mode className is present, those styles will be used:
h1 {
padding: 0.5rem;
border: 3px dotted red;
}
.dark-mode {
background: black;
color: white;
}
.dark-mode h1 {
border-color: aqua;
}
Ant Design newly start to support dynamic theme support. But its on experimental usage. You can find details on this link.
Conditional require won't block using previously required module. So, whenever your condition matches the require will available in your app. So, your both required module will be used. Instead of requiring them, insert stylesheet and remove to toggle between them:
const head = document.head
const dark = document.createElement('link')
const light = document.createElement('link')
dark.rel = 'stylesheet'
light.rel = 'stylesheet'
dark.href = 'antd.dark.css'
light.href = 'antd.light.css'
useEffect(() => {
const timer = setTimeout(() => {
if (darkMode) {
if (head.contains(light)) {
head.removeChild(light)
}
head.appendChild(dark)
} else {
if (head.contains(dark)) {
head.removeChild(dark)
}
head.appendChild(light)
}
}, 500)
return () => clearTimeout(timer)
}, [darkMode])
This package will help you to export and use theme vars without losing performance
Using less compiler in runtime:
https://medium.com/#mzohaib.qc/ant-design-dynamic-runtime-theme-1f9a1a030ba0
Import less code into wrapper
https://github.com/less/less.js/issues/3232
.any-scope {
#import url('~antd/dist/antd.dark.less');
}

passing mixins to styled-component

I am trying to pass a mixin from Material UI to a styled-component. The issue is that I can't figure out a way to pass the mixin value to the styled-component without assigning it to to a css property. For example, this is not possible:
const Example = styled.div`
${p => p.theme.mixins.toolbar};
`;
Edit: The issue ended up being the semi-colon next to the closing '}'. I believe adding a semi colon makes the styled-component think that you are adding a normal property, not a mixin.
You need to spread the mixins not calling it, like so:
const Example = styled.div`
${props => ({ ...props.theme.mixins.toolbar })}
`;
Yet that will return style object, you might wanna convert the resulting object into css-compatible syntax as follows:
const Example = styled.div`
${props => (Object.entries({ ...props.theme.mixins.toolbar }).reduce((styleString, [propName, propValue]) => {
if (propName.indexOf('#') !== -1) {
// iterate over media queries
return `${styleString}${propName} { ${Object.entries(propValue).reduce((ss, [pn, pv]) => {
pn = pn.replace(/([A-Z])/g, m => `-${m[0].toLowerCase()}`);
return `${ss}${pn}:${pv+(Number.isInteger(pv) ? 'px' : '')};`;
}, '')}; }`;
}
propName = propName.replace(/([A-Z])/g, matches => `-${matches[0].toLowerCase()}`); // convert camel-case properties into dash-splitted attributes
return `${styleString}${propName}:${propValue+(Number.isInteger(propValue) ? 'px' : '')};`; // append css pixel unit to integer values
}, ''))}
`;
I read your edit but deleting the semicolon didn't work for me. This worked:
import React from "react";
import { createMuiTheme } from "#material-ui/core/styles";
import styled, { ThemeProvider } from "styled-components";
const Content = styled.div`
toolbar: ${props => props.theme.mixins.toolbar};
`;
const theme = createMuiTheme();
const Wrapper = props => {
return (
<ThemeProvider theme={theme}>
<Content children={props.children} />
</ThemeProvider>
);
};
export default Wrapper;
I read this documentation, and very simple code:
const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
function App() {
return (
<React.Fragment>
<AppBar position="fixed">
<Toolbar>{/* content */}</Toolbar>
</AppBar>
<Offset />
</React.Fragment>
);
}

Resources