How to Create Dynamic Media Queries with Styled-Components? - reactjs

I am trying to use the following approach below to not cause memory overload:
${MediaXS} {
font-size: 0.77rem;
}
Instead of using this approach below, which causes memory problems because it is an array:
$ {MediaXS`
color: red
`}
But I don't know the correct way to export to use with the first example, what should I do?
MY CODE:
import { css } from "styled-components";
export const MediaXS = css`
#media only screen and (min-width: 320px) {
}
`;
export const MediaSM = css`
#media only screen and (min-width: 576px) {
}
`;
export const MediaMD = css`
#media only screen and (min-width: 768px) {
}
`;
export const MediaLG = css`
#media only screen and (min-width: 992px) {
}
`;
export const MediaXL = css`
#media only screen and (min-width: 1200px) {
}
`;

I believe you can do something like this:
// configure screen width thresholds
const ScreenSizes = {
DESKTOP: 992,
TABLET: 768,
PHONE: 576
}
const sizes = {
desktop: ScreenSizes.DESKTOP,
tablet: ScreenSizes.TABLET,
phone: ScreenSizes.PHONE
}
// iterate through sizes and create a media template
const media =
Object
.keys(sizes)
.reduce((acc, label) => {
acc[label] = (...args) => css`
#media (max-width: ${sizes[label] / 16}em) {
${css(...args)}
}
`
return acc
}, {});
// use media methods with styled-component instead of raw #media queries
const StyledContent = styled.div`
width: 100%
${media.desktop`
padding: 10px;
`}
${media.tablet`
padding: 15px;
max-width: 700px;
`}
${media.phone`
padding: 20px;
max-width: 900px;
`}
`;
export class Content extends React.Component {
render() {
return (
<StyledContent>
{ this.children }
</StyledContent>
);
}
}
Credits to Ross Bulat, taken from this article:
https://medium.com/#rossbulat/creating-themes-in-react-with-styled-components-6fce744b4e54

I'm using dinamics media with styled-componets of this form:
const tabletStartWidth = 600;
const desktopStartWidth = 768;
const extraLargeStartWidth = 1300;
const responsiveDesign = {
tablet: `#media (min-width: ${tabletStartWidth}px) and (max-width: ${desktopStartWidth - 1}px)`,
desktop: `#media (min-width: ${desktopStartWidth}px) and (max-width: ${extraLargeStartWidth - 1}px)`,
extraLarge: `#media (min-width: ${extraLargeStartWidth}px)`,
};
and in the component e.g:
${responsiveDesign.desktop} {
padding: 5px;
justify-content: "center";
text-align: center;
}
${responsiveDesign.extraLarge} {
padding: 10px;
}
And it works for me.

Related

Implementing animation when removing Toast

I have a working ToastList that enables me to click a button multiple times and generate a toast each time. On entry, I have an animation, but when I remove the toast, I do not get an animation. I am using Typescript and functional components.
My component is as follows:
import React, { useCallback, useEffect, useState } from 'react';
import * as Styled from './Toast.styled';
export interface ToastItem {
id: number;
title: string;
description: string;
backgroundColor: string;
}
export interface ToastProps {
toastList: ToastItem[];
setList: React.Dispatch<React.SetStateAction<ToastItem[]>>;
}
export default function Toast(props: ToastProps) {
const deleteToast = useCallback(
(id: number) => {
const toastListItem = props.toastList.filter((e) => e.id !== id);
props.setList(toastListItem);
},
[props.toastList, props.setList]
);
useEffect(() => {
const interval = setInterval(() => {
if (props.toastList.length) {
deleteToast(props.toastList[0].id);
}
}, 2000);
return () => {
clearInterval(interval);
};
}, [props.toastList, deleteToast]);
return (
<Styled.BottomRight>
{props.toastList.map((toast, i) => (
<Styled.Notification
key={i}
style={{ backgroundColor: toast.backgroundColor }}
>
<button onClick={() => deleteToast(toast.id)}>X</button>
<div>
<Styled.Title>{toast.title}</Styled.Title>
<Styled.Description>{toast.description}</Styled.Description>
</div>
</Styled.Notification>
))}
</Styled.BottomRight>
);
}
And my styling is done using styled-components and is as follows:
import styled, { keyframes } from 'styled-components';
export const Container = styled.div`
font-size: 14px;
position: fixed;
z-index: 10;
& button {
float: right;
background: none;
border: none;
color: #fff;
opacity: 0.8;
cursor: pointer;
}
`;
const toastEnter = keyframes`
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
`;
export const BottomRight = styled(Container)`
bottom: 2rem;
right: 1rem;
`;
export const Notification = styled.div`
width: 365px;
color: #fff;
padding: 15px 15px 10px 10px;
margin-bottom: 1rem;
border-radius: 4px;
box-shadow: 0 0 10px #999;
opacity: 0.9;
transition .1s ease;
animation: ${toastEnter} 0.5s;
&:hover {
box-shadow: 0 0 12px #fff;
opacity: 1;
}
`;
export const Title = styled.p`
font-weight: 700;
font-size: 16px;
text-align: left;
margin-bottom: 6px;
`;
export const Description = styled.p`
text-align: left;
`;
When I click a button, I just add an element to the state list, like:
toastProps = {
id: list.length + 1,
title: 'Success',
description: 'Sentence copied to clipboard!',
backgroundColor: '#5cb85c',
};
setList([...list, toastProps]);
My component is rendered like:
<Toast toastList={list} setList={setList}></Toast>
I would like to add animation when a toast exits, but do not know how. I have tried changing the style according to an additional prop I would send to the styled components, but this way all the toasts animate at the same time. My intuition is that I should use useRef(), but I am not sure how. Thanks in advance for any help you can provide.

How can i keep my changes after not Intersecting to my element?

I'm using react and I want to make some animation when the viewport is on my desired element,
but I only want to fire it once so when observer value is false, my element style not changing again to beginning .
I'm using styled-components for styling
HomeSection.Projects = function HomeSectionProjects({ children, ...rest }) {
const ref = useRef(null);
const [isVisible, setVisible] = useState(false);
const callbackFn = (entries) => {
const [entry] = entries;
setVisible(entry.isIntersecting);
};
useEffect(() => {
const options = {
root: null,
rootMargin: "0px",
threshold: 1,
};
const observer = new IntersectionObserver(callbackFn, options);
if (ref.current) observer.observe(ref.current);
}, [ref]);
return <Projects ref={ref}>{children}</Projects>;
};
So here is my styled-comp
export const Projects = styled.div`
width: 70%;
height: 70vh;
display: flex;
opacity: 1;
margin-bottom: 30vh;
justify-content: flex-start;
align-items: center;
position: relative;
transition: opacity 1.5s ease-out;
#media screen and (max-width: 763px) {
margin-bottom: 20%;
height: 50vh;
align-items: center;
flex-direction: column;
padding-top: 10%;
width: 100%;
}
&:hover ${ImageDiv} {
transform: scale(0.9);
}
&:hover ${Title} {
/* transform: scale(1.4); */
font-size: 8rem;
#media screen and (max-width: 763px) {
font-size: 3.2rem;
}
}
&:hover ${TitleInfo} {
font-size: 2rem;
bottom: -5%;
#media screen and (max-width: 763px) {
font-size: 1.5rem;
}
}
&:hover ${Info} {
left: -8%;
#media screen and (max-width: 763px) {
}
}
`;
How can I achieve my goal here?
You can stop observing the element once it's visible. Also your callbackFn doesn't have to be outside in the function body.
useEffect(() => {
const options = {
root: null,
rootMargin: "0px",
threshold: 1,
};
const callbackFn = (entries) => {
const [entry] = entries;
if(entry.isIntersecting)
{
setVisible(true);
observer.unobserve(entry.target);
}
};
const observer = new IntersectionObserver(callbackFn, options);
if (ref.current) observer.observe(ref.current);
}, [ref]);

How to use Media Queries inside a React Styled Components Keyframe?

I can get media queries to work properly inside a regular styled-components component however when I attempted to use it in a keyframe (via import from styled-components) it does not seem to work at all.
Attempting to get a div to animate to a specific position, but have that end position change when the window is < 800px, I've attempted this:
import styled, { keyframes } from 'styled-components';
// ... further down ...
const slideAnim = keyframes`
100% {
top: 20px;
left: 30px;
}
#media (max-width: 800px) {
top: 70px;
left: 50px;
}
`;
I have also tried putting the media query in the 100% block:
const slideAnim = keyframes`
100% {
top: 20px;
left: 30px;
#media (max-width: 800px) {
top: 70px;
left: 50px;
}
}
`;
I made a helpful interactive demonstration of what I am trying to achieve (problem code is at line 24):
https://codesandbox.io/embed/fragrant-star-m71ct
Feel free to change the breakpoint variable from 800 if you need to.
Any help is appreciated!
You could take a different approach, where your keyframes animation is instead defined as a function like this:
const slideAnim = (top, left) => keyframes`
100% {
top: ${ top };
left: ${ left };
}
`;
The function accepts input parameters that dictate the destination coordinates of top and left for that animation's final keyframe.
In your stlyed component (ie Box1), you'd then invoke slideAnim() with specific coordinates to each breakpoint, to achieve different animation behavior per breakpoint:
/*
Declare responsive styling in the Box1 component, where destination
coordinates are specified for the animate on a per-breakpoint-basis
*/
const Box1 = styled.div`
width: 20px;
height: 20px;
background-color: blue;
position: absolute;
left: -100px;
top: 80px;
&.slide {
animation: ${slideAnim('20px', '30px')} 0.4s ease-in-out forwards;
#media (max-width: ${breakpoint}px) {
animation: ${slideAnim('70px', '50px')} 0.4s ease-in-out forwards;
}
}
`;
In summary, the idea is to shift the responsive styling into your styled component (ie Box1), while defining a reusable function that contains the common/shared keyframes for each responsive breakpoint.
Here's a working example - hope that helps!
A more developed approach to use media queries with styled-components
firstly we define the screen size for each device:
const size = {
mobile: "320px",
tablet: "768px",
laptop: "1024px",
desktop: "2560px",
}
then we use css imported from styled-components to address media queries in relevant constants for each device
export const mobile = (inner) => css`
#media (max-width: ${size.mobile}) {
${inner};
}
`;
export const tablet= (inner) => css`
#media (max-width: ${size.tablet}) {
${inner};
}
`;
export const desktop= (inner) => css`
#media (max-width: ${size.desktop}) {
${inner};
}
`;
export const laptop= (inner) => css`
#media (max-width: ${size.laptop}) {
${inner};
}
`;
now lets say you are creating a styled div and you want to use media queries it. and based on the accepted answer to use keyframes
const slideAnim = (top, left) => keyframes`
100% {
top: ${ top };
left: ${ left };
}
`;
const StyledDiv= styled.div`
/* ...css */
animation: ${slideAnim('20px', '30px')} 0.4s ease-in-out forwards;
${mobile(css`
width:300px;
animation: ${slideAnim('10px', '20px')} 0.1s ease-in-out forwards;
/* ...css */
`)};
${tablet(css`
width:500px;
/* ...css */
`)};
/*... and so on */
`;
Use keyframes outside of all blocks. eg:
import { IconButton} from "#material-ui/core";
const CloseButton = styled(IconButton)`
padding: 3px !important;
outline: none;
#media only screen and (min-width: 728px) {
padding: 4px !important;
margin:0
}
`;

Gatsby error Cannot read property 'fixed' of null

I want to show my Products in the Menu section of index page . I try to display content such as text and images from contentful API but I get this error:
TypeError: Cannot read property 'fixed' of null
src/components/HomePageComponents/Product.js:6
3 | import { styles } from "../../utils"
4 | import Img from "gatsby-image"
5 |
6 | export default function Product({ product }) {
7 | const { name, price, ingredients } = product
8 | const { fixed } = product.img
This is my Product.js
import React from "react"
import styled from "styled-components"
import { styles } from "../../utils"
import Img from "gatsby-image"
export default function Product({ product }) {
const { name, price, ingredients } = product
const { fixed } = product.img
return (
<ProductWrapper>
<Img fixed={fixed} className="img" />
<div className="text">
<div className="product-content">
<h3 className="name">{name}</h3>
<h3 className="price">{price}</h3>
</div>
<p className="info">{ingredients}</p>
</div>
</ProductWrapper>
)
}
const ProductWrapper = styled.div`
#media (min-width: 576px) {
display: grid;
grid-template-columns: auto 1fr;
grid-column-gap: 1rem;
}
.img {
border-radius: 0.5rem;
}
.product-content {
display: flex;
justify-content: space-between;
font-size: 1.4rem;
text-transform: uppercase;
}
.name {
color: ${styles.colors.mainYellow};
margin-top: 0.5rem;
}
.price {
color: ${styles.colors.mainYellow};
margin-top: 0.5rem;
}
.info {
margin-top: 0.5rem;
word-spacing: 0.2rem;
text-transform: lowercase;
}
`
this is my Menu.js
import React from "react"
import Product from "./Product"
import { StaticQuery, graphql } from "gatsby"
import { Section, Title } from "../../utils"
import styled from "styled-components"
// import { Link } from "gatsby"
export default function Menu() {
return (
<Section>
<Title title="Featured items" message="Little taste" />
<ProductList>
<StaticQuery
query={graphql`
{
items: allContentfulMenu {
edges {
node {
name
price
id
ingredients
img {
fixed(width: 150, height: 150) {
...GatsbyContentfulFixed_tracedSVG
}
}
}
}
}
}
`}
render={data => {
return data.items.edges.map(item => {
return <Product key={item.node.id} product={item.node} />
})
}}
/>
</ProductList>
</Section>
)
}
const ProductList = styled.div`
margin: 3rem 0;
display: grid;
grid-template-columns: 100%;
grid-row-gap: 3rem;
#media (min-width: 576px) {
grid-template-columns: 95%;
}
#media (min-width: 776px) {
grid-template-columns: 80%;
justify-content: center;
}
#media (min-width: 992px) {
grid-template-columns: 1fr 1fr;
grid-gap: 2rem;
}
`
It seems that there's no image specified at the Contentful field. Therefore the fixed value is null and that's not allowed by their plugin gatsby-source-contentful.
In this case looks like the property img in product don't have any fixed property. In particular, img is null, and you're treating it as an object. You may have to check if img is actually an object before destructuring it

Reuse const object in react

Say I would want to have the same codeblock in an object, with an addition:
const styles = {
styleA: `
background-color: red;
color: silver;
`,
styleB: `
background-color: green;
color: white;
`,
styleC: `
background-color: red;
color: silver;
font-size: 16px;
`
};
As you can see, styleA and styleC are similar, exept for the fontsize addition to styleC. How can I rewrite this with react (es6) so that
styleA = styleC + 'font-size: 16px'; ?
Or is there a better way of doing this altogether?
You can use the css helper from styled-components to help create styles and mixins from other readymade components.
For example, in your case:
const styleA = css`
background-color: red;
color: silver;
`;
const styleB = css`
background-color: yellow;
`;
const styles = {
styleC: css`
${styleA}
${styleB}
font-size: 32px;
`
};
const MainHeading = styled.h2`
${styles.styleC}
`;
you can have something like this.
const createStyle = (bgColor, color, fontSize) => {
return (
`
background-color: ${bgColor};
color: ${color};
${fontSize ? `font-size: ${fontSize}` : null}; //if there is fontSize, return it
`
);
}
thus:
const styles = {
styleA: createStyle('red', 'silver'),
styleB: createStyle('red', 'silver', '16px')
};

Resources