Are there any best practices for using styled-jsx with responsive design (media queries)?
I'm experimenting with react-responsive but it doesn't seem to accept <style jsx> inside like:
<MediaQuery query='(max-width: 600px)'>
<style jsx>{`
.my-element {
background-color: tomato;
}
`}</style>
</MediaQuery>
I've been using cssnext via postcss, and postcss-import. This lets me have the following pattern:
styles/variables.css
@custom-media --small-only (width < 640px);
@custom-media --small-only-height (height < 640px);
@custom-media --medium-up (width > 641px);
@custom-media --medium-down (width < 641px);
@custom-media --medium-only (width > 641px) and (width < 1024px);
@custom-media --medium-up-height (height > 641px);
@custom-media --medium-down-height (height < 641px);
@custom-media --medium-only-height (height > 641px) and (height < 1024px);
@custom-media --large-up (width > 1025px);
@custom-media --large-down (width < 1025px);
@custom-media --large-only (width > 1025px) and (width < 1440px);
@custom-media --large-up-height (height > 1025px);
@custom-media --large-down-height (height < 1025px);
@custom-media --large-only-height (height > 1025px) and (height < 1440px);
@custom-media --xlarge-up (width > 1441px);
@custom-media --xlarge-down (width < 1441px);
@custom-media --xlarge-only (width > 1441px) and (width < 1920px);
@custom-media --xlarge-up-height (height > 1441px);
@custom-media --xlarge-down-height (height < 1441px);
@custom-media --xlarge-only-height (height > 1441px) and (height < 1920px);
@custom-media --xxlarge-up (width > 1921px);
@custom-media --xxlarge-down (width < 1921px);
@custom-media --xxlarge-only (width > 1921px) and (width < 9999999px);
@custom-media --xxlarge-up-height (height > 1921px);
@custom-media --xxlarge-down-height (height < 1921px);
@custom-media --xxlarge-only-height (height > 1921px) and (height < 9999999px);
components/Footer.js
...
<style jsx>{`
@import "../styles/variables.css";
footer {
background: black;
@media (--medium-up) {
padding: 0rem 3rem;
}
}
`}</style>
...
I'm digging this approach, since it's still all just CSS. I'm running into issues when I do npm run build though. postcss-import is crapping out, I think.
Setting path: ['./'] in postcss-import's config fixed my next build woes.
@tomsoderlund that's an interesting use case because it makes totally sense to me but it is a bit complex for styled-jsx which works as follow:
We grab only the top level style tags that are children (direct descendant) of the main jsx tag (an element eg. div)
<div>
hi
<style jsx>...</style>
</div>
Nested tags are not collected eg:
<div>
<span>
hi
<style jsx>{/* not collected and transpiled*/}</style>
</span>
<style jsx>...</style>
</div>
The main reason is that styles are eventually deduped and shared across instances so if we were to allow nested style tags an instance could unmount styles that other instances need e.g.
{this.state.something && <Media><style jsx>{'div { color: red }'}</style></Media>}
In your case you have two options:
<MediaQuery query='(max-width: 600px)'>
<ComponentWithSmallStyles />
</MediaQuery>
<MediaQuery query='(min-width: 600px)'>
<ComponentWithLargeStyles />
</MediaQuery>
@nickdandakis I was having the same problem as OP, but really like your approach. Having some trouble getting it to work with the examples though. Cold you share your config?
Sure thing @por.
I'm using the following npm packages:
styled-jsx-postcss
postcss-cssnext
postcss-cssimport
My postcss.config.js
module.exports = () => ({
plugins: [
require('postcss-import')({
path: ['./']
}),
require('postcss-cssnext')({})
]
})
My .babelrc
{
"presets": [
"./utils/babel-preset.js"
]
}
This is kind of iffy, but it's how I set up styled-jsx-postcss with Next.js. I can't find any documentation for this anymore, so maybe it's uneccessary and you can just enable it by setting it as a plugin in your .babelrc. utils/babel-preset.js contains the following.
// installs PostCSS
const nextBabelPreset = require('next/babel');
nextBabelPreset.plugins = nextBabelPreset.plugins.map(plugin => {
if (!Array.isArray(plugin) && plugin.indexOf('styled-jsx/babel') !== -1) {
return require.resolve('styled-jsx-postcss/babel');
}
return plugin;
})
module.exports = nextBabelPreset;
I remember getting some postcss version conflicts between the loaders and styled-jsx-postcss, so here's the versions I'm using:
"postcss-cssnext": "^2.11.0",
"postcss-import": "^9.1.0",
"styled-jsx-postcss": "^0.2.0"
Let me know if this works out for ya. Took me a little bit to get the config working the way I wanted it to!
@nickdandakis Thanks! I got it working. I actually had those settings, but I didn鈥檛 realise that changes in the variables.css file don鈥檛 update styles on refresh and changes to styled jsx seem to be cached. Bit of a tricky setup indeed. I did manage to update to all the latest versions of those plugins without a problem. Thanks again for the help and quick response!馃嵒
Ah yeah, clearing npm cache helps with that. Glad I could help!
Nick's solution is cool but config seems a bit more complicated than it needs to be.
npm i styled-jsx-plugin-postcss postcss-cssnext postcss-import
// ./package.json
// ...
"babel": {
"presets": [
"next/babel",
"env"
],
"plugins": [
[
"styled-jsx/babel",
{
"plugins": [
"styled-jsx-plugin-postcss"
]
}
]
]
},
// ...
// ./postcss.config.js
module.exports = (ctx) => ({
plugins: {
'postcss-import': {
path: `./pages`,
},
'postcss-cssnext': {},
},
})
// ./pages/styles/vars.css
:root {
--mainColor: blue;
}
// ./pages/components/any/level/deep/Header.js
<div>
<h1>Yo</h1>
<style jsx>`{
@import 'styles/vars.css';
h1 {
background: var(--mainColor);
}
}`</style>
</div>
@giuseppeg Actually I just noticed the caching issue here https://github.com/zeit/styled-jsx/issues/254#issuecomment-314908379
For instance, if I change --mainColor to red, it will not update, even when I restart $ next.
Any idea can we fix this?
@por How did you end up solving this?
Fwiw, I don't even care if I have to delete directories just to flush this (as a bandage). I just don't know what files to delete and such (no matter what I try it seems like these css variables are cached). 馃槵
rm -rf {.next,node_modules}; npm i; npm run dev reset it.
@corysimmons have you tried the following?
rm -rf node_modules/.cache .next
npm run dev
@corysimmons you're very correct. Since I've set this up, styled-jsx has first class citizen support for plugins which makes setup a lot easier (like your example!).
Imported stylesheets get heavily cached, so yes, you have to rm -rf node_modules/.cache and restart your dev server to see changes in them. Annoying, but hasn't been a detriment to workflow as we barely modify our global stylesheets.
if it can help you can define a predev script to do the cleanup automatically:
"scripts": {
"predev": "rm -rf .node_modules/.cache .next",
"dev": "next"
}
@nickdandakis
Imported stylesheets get heavily cached, so yes, you have to rm -rf node_modules/.cache and restart your dev server to see changes in them. Annoying, but hasn't been a detriment to workflow as we barely modify our global stylesheets.
Yeah I'm doing this now.
@giuseppeg lol, I'm doing almost that exactly, but it's still a little annoying to have to restart dev every time... if you don't have plans to fix it I wonder if I could just chokidar-cli, watch ./pages/styles/..., and restart dev... Still feels wrong but might be better...
Please fix tho as this is an otherwise perfect workflow. :X
@corysimmons I think that this is an "issue" with babel-loader. You may want to try to set its cacheDirectory option to false to avoid caching (obviously every time you make a change then it will rebuild everything).
Anyway this is going out of topic :) I think that option 1) and 2) from my comment https://github.com/zeit/styled-jsx/issues/254#issuecomment-313841098 are a good way to go.
If you need now you can also use dynamic styles eg.
({children}) => {
const small = window.matchMedia('max-width: 600px').matches
return (
<div>
{children}
<style jsx>{`div { background: ${small ? 'tomato' : 'hotpink' }`}</style>
</div>
)
}
@giuseppeg Actually rm -rf node_modules/.cache .next doesn't seem to work. I'm not sure why rm -rf {.next,node_modules}; npm i; npm run dev does...
@giuseppeg When you use window.matchMedia('max-width: 600px').matches, do you have issues with your component when the user resizes the window? Because that changes the width of the viewport, but I don't think the component would be triggered to re-render because the width used inside the component is a static variable
@joncursi that's correct. You'd need do add an event listener and keep a boolean in the state:
class Media extends React.Component {
_media = window.matchMedia(this.props.query)
state = {
matches: this._media.matches
}
onChange = ({matches}) => { this.setState({ matches }) }
componentDidMount() {
this._media.addListener(onChange)
}
componentWillUnmount() {
this._media.removeListener(onChange)
}
render() {
return this.state.matches ? children : null
}
}
which you can use like this
<Media query="max-width: 600px"><MyComponent /></Media>
I didn't test or try the code above but it should work. I believe that https://www.npmjs.com/package/react-media is implemented in a similar way
Note to self:
```
class MQL extends React.Component {
constructor(props) {
super(props);
this.state = { matches: false };
this.mql = null;
}
componentDidMount() {
if (!window.matchMedia) {
return;
}
const mql = window.matchMedia(this.props.mediaQuery);
mql.addListener(this.mediaQueryChanged);
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ mql, matches: mql.matches });
}
componentWillUnmount() {
if (!window.matchMedia) {
return;
}
if (this.state.mql) {
this.state.mql.removeListener(this.mediaQueryChanged);
}
}
mediaQueryChanged = () => {
const { matches } = this.state.mql;
const state = { matches };
this.setState(state);
};
render() {
return this.props.children(this.state);
}
}
{({ matches }) => (
)}
```
Example using react-media and className toggling:
// Example component
import Media from 'react-media';
const Example = () => (
<Media query="(min-width: 1025px)">
{matches => (
<div className={matches ? 'desktop' : ''}>
<style jsx>
{`
div {
padding-top: 40px;
}
.desktop {
padding-top: 80px;
}
`}
</style>
</div>
)}
</Media>
);
export default Example;
Most helpful comment
if it can help you can define a
predevscript to do the cleanup automatically: