Props, TailwindCSS & Twin.Macro - reactjs

I am trying to get the following component to work properly with Twin.Macro:
import "twin.macro";
const Pattern = ({ classes }) => {
return (
<div css={[classes.wrapper]}>
<img
src="..."
css={[classes.img]}
/>
</div>
);
};
In other words, I would like to receive the props via the classes prop and pass the values on to Twin.Macro. However, it is not working properly. For example, here is an implementation of that component:
<Pattern
classes={{
wrapper: "absolute -left-2 z-10 lg:left-0 top-0 h-full bg-contain bg-line-left-md lg:bg-line-left bg-no-repeat xl:bg-line-left-md hidden sm:block",
img: "h-full",
}}
/>
And here is the css that is output:
.css-w36jn9-Pattern {
absolute -left-2 z-10 lg: left-0 top-0 h-full bg-contain bg-line-left-md lg:bg-line-left bg-no-repeat xl:bg-line-left-md hidden sm:block;
}
As you can see, all it does is pass on the values of the tailwind css classes. It does not process those classes in the proper property-value pairs (such as position: absolute; left: -2px, etc).
I have tried different ways to get this to work, but nothing has been successful. Any idea what I am doing wrong and how to fix it?
Thanks.

accordingly to Twin.Macro when passing css property, you need to pass your classes to tw function using tagged template literals. given you are passing a variable use also string interpolation.
finally, your code should look like:
import tw from "twin.macro";
const Pattern = ({ classes }) => {
return (
<div css={[tw`${classes.wrapper}`]}>
<img
src="..."
css={[tw`${classes.img}`]}
/>
</div>
);
};

Related

Tailwind CSS unresponsive to react state change

This very simple toggle in react is not working with tailwind CSS. When I click the "toggle color" button, the className of the div changes between bg-[#222222] and bg-[#DDDDDD], but the div never actually colors -- it has the className, but tailwind doesn't seem to be able to fill in the background property.
import React, { useState } from "react";
function App() {
const [color, setColor] = useState('222222')
const toggleColor = () => color === '222222' ? setColor('DDDDDD') : setColor('222222');
return (
<div className='h-screen w-full flex flex-col justify-center items-center'>
<button onClick={toggleColor}>Toggle Color</button>
<div className={`bg-[#${color}] w-[100px] h-[100px]`}></div>
</div>
)
}
export default App;
To clarify, if I store the full string 'bg-[#2222222]' or 'bg-[#DDDDDD]' and then drop that directly into the className, it works. (AKA, in the below code segment, the div renders with the proper color and toggles properly:
import React, { useState } from "react";
function App() {
const [color, setColor] = useState('bg-[#222222]')
const toggleColor = () => color === 'bg-[#222222]' ? setColor('bg-[#DDDDDD]') : setColor('bg-[#222222]');
return (
<div className='h-screen w-full flex flex-col justify-center items-center'>
<button onClick={toggleColor}>Toggle Color</button>
<div className={`${color} w-[100px] h-[100px]`}></div>
</div>
)
}
export default App;
However, in the larger context of the application I'm hoping to build, this doesn't do me much good. It would be much more helpful to simply store the color. It's unclear to me why the first way is not working, considering that in both cases, the full string is successfully dropped into the className. Any ideas?
This is the expected behaviour of Tailwind and the reason is explained here: https://tailwindcss.com/docs/content-configuration#class-detection-in-depth
One option would be to use a safe list which is described here: https://tailwindcss.com/docs/content-configuration#safelisting-classes
Another solution for an exception like this is to ignore Tailwind and add a dynamic style to the element. For example:
<div className={'w-[100px] h-[100px]'} style={{'background-color': `#${color}`}}></div>
This might be more flexible than safelisting Tailwind classes, if you have a lot of colors or if you don't know the color value until runtime.

Tailwind CSS custom gradient with React state and FastAverageColor

I'm trying to add a custom gradient over an image using React state, Tailwind CSS and the FastAverageColor package (https://www.npmjs.com/package/fast-average-color) into my Next JS app.
I'm using an useEffect hook for this:
const [avgColor, setAvgColor] = useState("")
useEffect(() => {
const fac = new FastAverageColor()
fac.getColorAsync(songData.track.album.images[0].url, { algorithm: 'dominant' }).then(result => {
setAvgColor(`w-full h-full absolute bg-gradient-to-tr from-[${result.hex}] to-transparent`)
})
}, [avgColor, songData.track.album.images])
The JSX is presented below:
<div className="relative w-full h-full">
<Image priority alt="test" layout='fill' objectFit='cover' src={songData.track.album.images[0].url} />
{avgColor ? <div className={avgColor}></div> : null}
</div>
The problem is that my gradient doesn't appear over the image. Do you know maybe why this happens?
It's not possible to do with JIT classes but you can use from-current and set an inline color to set the from color.
So, try this:
setAvgColor(result.hex)
and
<div style={{color: avgColor}} className="w-full h-full absolute bg-gradient-to-tr from-current to-transparent"></div>
basic demo

Conditionally set background color in React component with Tailwind CSS

I am trying to use a hex color code passed through props to set the background color of a div. These are one-off colors that are generated dynamically, so cannot be added as a theme extension in tailwind.config.
I thought a template literal would be the best way to achieve this, but have not been able to get this to work with arbitrary color values in Tailwind CSS.
interface Props {
color: string;
}
const ColorSwatch = ({ color }: Props) => {
return (
<div className="flex flex-col gap-1 p-2">
<div
className={`h-20 w-20 border border-gray-400 shadow-md bg-[${color}]`}
></div>
<p className="text-center">{color}</p>
</div>
);
};
export default ColorSwatch;
Pasting the hex color code directly into the className list produces expected results, but trying to use the prop value in a template literal results in a transparent background (no background effect applied).
Looking for advice on how to correct this or different approaches to dynamically setting background color with a hex code passed through props.
Unfortunately, Tailwind requires the color to be hardcoded into the className prop as it cannot compute arbitrary styles from dynamic className values.
Your best bet would be to set the background color using the style prop as shown below;
interface Props {
color: string;
}
const ColorSwatch = ({ color }: Props) => {
return (
<div className="flex flex-col gap-1 p-2">
<div
className="h-20 w-20 border border-gray-400 shadow-md"
style={{backgroundColor: color}}
></div>
<p className="text-center">{color}</p>
</div>
);
};
export default ColorSwatch;
You can look here and here to read more on how Tailwind generates arbitrary styles.

Use React variables in Tailwind class names

I've just recently picked up React and Tailwind for a project, and I am still very much a beginner. I wanted to make an element have a background image as a custom class variable, something like this:
<div className="bg-[url(`https://example.com/${variable}.png`)]"></div>
But as Tailwind purges classes, would this somehow be possible? I hope I'm not missing anything, but it doesn't seem doable to me right now
Tailwind has a feature called "safelisting" which may provide you with the functionality you need – see here in the docs.
That said:
1 - There's nothing wrong with using both className and style props on the same element – you can have e.g. <div className="w-full" style={`background: url(https://example.com/${variable}.png`}>
2 – Depending on what flavor of React you are using, your might be better off using a component like Next.js's Image to optimize your images instead of setting background via CSS. Below is how I do it:
import Image from "next/image";
export default function MyBackground(props) {
return (
<div
className="z-0 absolute inset-0 w-full h-full flex justify-center items-center"
>
<Image
placeholder="blur"
alt={props.alt}
src={props.src}
className="h-full w-full object-cover"
/>
</div>
);
}
You can define a different variable for this. In that way, you can manipulate the value accordingly and use that variable to set the styling properties of that element. The code will look something like this:
const YourComponent = () => {
//the variable named variable will hold the dynamic value as per your need
let elementStyle = "bg-[url(`https://example.com/" + variable + ".png";
return (<>
<div className={elementStyle}"></div>
</>
);
}

ReactJs/NextJs - Conditionally render mutually exclusive components without compromising load time

I am trying to improve loading speed of a react web app.
I have two component imports - one for mobile and one for desktop (Bad design? I think so):
import Posts from '../components/post/posts';
import PostsMobile from '../components/post/postsMobile';
This was easy for development because I did not have to try hard to make the same component compatible for desktop and mobile.
Then to check screen size and load the appropriate component, I do this:
const largeScreen = useMediaQuery(theme => theme.breakpoints.up('sm'));
...
{largeScreen? (
<Posts />
) :
(
<PostsMobile />
)
}
You can resize the browser here to see the two components load: Link to home page showing the two components
Does <PostsMobile /> get imported only when react sees that its needed OR does it automatically get imported in the beginning no matter what?
Is there a better way to conditionally render mutually exclusive components without compromising load time?
Classic conditional rendering, that's the appropriate way to do it, only one component will be added to the DOM in any case. As a side note it's not the best idea to have two different components for mobile vs desktop view, generally your html structure should be the same and you should use CSS for any layout changes (as per Google's suggestions - https://web.dev/responsive-web-design-basics/)
Check out #artsy/fresnel, they have a very straightforward example showing how to configure Next.js and Gatsby.js to achieve screen-width dependent control in SSR environments
Note: it hardly increases overhead size; I am using it in my portfolio currently in conjunction with tailwindcss and it has proven to be a fantastic tool. Easy to configure and its implementation is straightforward. Here is an example of conditionally rendering the same svg icon four times as a function of screen size to customize styles accordingly (xs (mobile), sm, md, greater than md (desktop))
(1) Create a window-width file in your components directory to configure #artsy/fresnel for global sharing
components/window-width.jsx or components/window-width.tsx
import { createMedia } from '#artsy/fresnel';
const PortfolioMedia= createMedia({
breakpoints: {
xs: 0,
sm: 768,
md: 1000,
lg: 1200,
},
})
// Generate CSS to be injected in the head using a styles tag (pages/_document.jsx or pages/_document.tsx)
export const mediaStyles = PortfolioMedia.createMediaStyle();
export const { Media, MediaContextProvider } = PortfolioMedia;
// https://github.com/artsy/fresnel/tree/master/examples/nextjs
Note, you can customize the breakpoints however you'd like; the following breakpoints are from my portfolio's configuration file. They overlap with Tailwind's breakpoints to keep them playing nicely together
// ...
const PortfolioMedia = createMedia({
breakpoints: {
xs: 0,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
});
// ...
(2) Wrap pages/index.jsx or pages/index.tsx with the MediaContextProvider component
// ...
import { MediaContextProvider } from 'components/window-width';
interface IndexProps {
allPosts: Post[];
allAbout: AboutType[];
allBlog: BlogType[];
}
const Index = ({ allPosts, allAbout, allBlog }: IndexProps) => {
const morePosts = allPosts.slice(0);
const moreAbout = allAbout.slice(0);
const moreBlog = allBlog.slice(0);
return (
<Fragment>
<MediaContextProvider>
<Lead />
<Head>
<title>{`${CLIENT_NAME} landing page`}</title>
</Head>
<div className='max-w-cardGridMobile md:max-w-cardGrid my-portfolioH2F grid mx-auto content-center justify-center items-center text-center'>
{morePosts.length > 0 && <Cards posts={morePosts} />}
</div>
<div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
{moreAbout.length > 0 && <AboutCoalesced abouts={allAbout} />}
</div>
<div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
{moreBlog.length > 0 && <BlogCoalesced blogs={allBlog} />}
</div>
<Footer />
</MediaContextProvider>
</Fragment>
);
};
export default Index;
// ...
(3) Finally, inject generated mediaStyles CSS into a style tag of type text/css in Next's Head
pages/_document.jsx or pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext
} from 'next/document';
import { mediaStyles } from 'components/window-width';
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html lang='en-US'>
<Head>
<meta charSet='utf-8' />
<link rel='stylesheet' href='https://use.typekit.net/cub6off.css' />
<style type='text/css' dangerouslySetInnerHTML={{ __html: mediaStyles }} />
</Head>
<body className='root'>
<script src='./noflash.js' />
<Main />
<NextScript />
</body>
</Html>
);
}
}
(4) Profit; Configuration complete
That's all there is to it. For the sake of clarity I threw in an actual example from one of my projects below.
(5) Bonus example - conditionally rendering ArIcon as a function of device-size
components/lead-arIcon.tsx
import { ArIcon } from 'components/svg-icons';
import Link from 'next/link';
import { Media } from 'components/window-width';
import { Fragment } from 'react';
import DarkMode from 'components/lead-dark-mode';
const ArIconConditional = (): JSX.Element => {
const arIconXs: JSX.Element = (
<Media at='xs'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='18vw' height='18vw' />
</a>
</Link>
</Media>
);
const arIconSm: JSX.Element = (
<Media at='sm'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='15vw' height='15vw' />
</a>
</Link>
</Media>
);
const arIconMd: JSX.Element = (
<Media at='md'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='12.5vw' height='12.5vw' />
</a>
</Link>
</Media>
);
const arIconDesktop: JSX.Element = (
<Media greaterThan='md'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full'
id='top'
aria-label='top'
>
<ArIcon
width='10vw'
height='10vw'
classNames={[
` antialised w-svgIcon max-w-svgIcon transform transition-all`,
' stroke-current',
` fill-primary`
]}
/>
</a>
</Link>
</Media>
);
const DarkModeToggler = (): JSX.Element => (
<div className='pt-portfolio text-customTitle transition-all transform -translate-y-mdmxSocial col-span-4 text-right -translate-x-portfolioPadding'>
<DarkMode />
</div>
);
const ArIconsCoalesced = (): JSX.Element => (
<Fragment>
<div className='relative block justify-between lg:w-auto lg:static lg:block lg:justify-start transition-all w-full min-w-full col-span-2'>
{arIconXs}
{arIconSm}
{arIconMd}
{arIconDesktop}
</div>
</Fragment>
);
return (
<Fragment>
<div className='select-none relative z-1 justify-between pt-portfolioDivider navbar-expand-lg grid grid-cols-6 min-w-full w-full container overflow-y-hidden overflow-x-hidden transform'>
<ArIconsCoalesced />
<DarkModeToggler />
</div>
</Fragment>
);
};
export default ArIconConditional;
You could try to lazy load the components.
posts;
componentDidMount() {
if (largeScreen) {
import('../components/post/posts').then(({ default: Posts }) => {
// ^^^^ make sure it has a default export
this.posts = Posts;
this.forceUpdate();
});
} else {
// here load the other component for lower screens
}
}
Then, inside render:
const Posts = this.posts;
return largeScreen? (
<Posts />
) : (
<PostsMobile />
);
Note: You will have to also add a resize listener, so if the screen reaches certain width, the another component will load and will get rendered.
Note2: If you don't care about SSR - you could either try React.lazy with Suspense: https://en.reactjs.org/docs/code-splitting.html#reactlazy
In my opinion there are very valid usages of Desktop vs. Mobile components, for example, although CSS versions are definitely almost always preferred, it might not make sense for drag and drop components that don't work on mobiles, mobile-only hamburger menus, etc.
React's lazy loading is the way to go (unless you need SSR):
import React, { Suspense } from 'react';
const Posts = React.lazy(() => import('../components/post/posts');
const PostsMobile = React.lazy(() => import('../components/post/postsMobile');
function MainComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
{ largeScreen? ( <Posts />) : <PostsMobile /> }
</Suspense>
</div>
);
}
This will guarantee that the component is only loaded when it is first rendered. Bonus tip: if you change the loading div to an animated loading placeholder, the UI of your application may be more pleasant.
None of the answers were compatible with SSR using Nextjs so I ended up using Dynamic Import feature. Looks very powerful but simple.
https://nextjs.org/docs/advanced-features/dynamic-import
import dynamic from 'next/dynamic'
const Posts = dynamic(() => import('../components/post/posts'),
{ loading: () => <LinearProgress /> });
const PostsMobile = dynamic(() => import('../components/post/postsMobile'),
{ loading: () => <LinearProgress /> });
This saved me a few milliseconds
I am not sure if there is a better option so I hope people will comment.

Resources