Dynamic classes with tailwind and clsx [duplicate] - css

I'm just learning React and Tailwind CSS and had a strange experience with CSS grid using Tailwind classes. I've made the buttons for a calculator, with the last Button spanning two columns:
App.js:
export default function App() {
return (
<div className="flex min-h-screen items-center justify-center bg-blue-400">
<Calculator />
</div>
);
}
Calculator.js
import { IoBackspaceOutline } from "react-icons/io5";
export const Calculator = () => {
return (
<div className="grid grid-cols-4 grid-rows-5 gap-2">
<Button>AC</Button>
<Button>
<IoBackspaceOutline size={26} />
</Button>
<Button>%</Button>
<Button>รท</Button>
<Button>7</Button>
<Button>8</Button>
<Button>9</Button>
<Button>x</Button>
<Button>4</Button>
<Button>5</Button>
<Button>6</Button>
<Button>-</Button>
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>+</Button>
<Button>0</Button>
<Button>.</Button>
<Button colSpan={2}>=</Button>
</div>
);
};
const Button = ({ colSpan = 1, rowSpan = 1, children }) => {
return (
<div
className={`col-span-${colSpan} row-span-${rowSpan} bg-white p-3 rounded`}
>
<div className="flex items-center justify-center">{children}</div>
</div>
);
};
This doesn't work (tested in Chrome):
Now here comes the weird part. I replaced the returned JSX from the App component with HTML from a Tailwind tutorial and deleted it again.
<div className="bg-blue-400 text-blue-400 min-h-screen flex items-center justify-center">
<div className="grid grid-cols-3 gap-2">
<div className="col-span-2 bg-white p-10 rounded">1</div>
<div className="bg-white p-10 rounded">2</div>
<div className="row-span-3 bg-white p-10 rounded">3</div>
<div className="bg-white p-10 rounded">4</div>
<div className="bg-white p-10 rounded">5</div>
<div className="bg-white p-10 rounded">6</div>
<div className="col-span-2 bg-white p-10 rounded">7</div>
<div className="bg-white p-10 rounded">8</div>
<div className="bg-white p-10 rounded">9</div>
</div>
</div>
After I Ctrl-Z'd a bunch of times, so I had only the previous code, my button suddenly spans two columns as intended:
I checked to make sure that there were no changes in the code:
My friend even cloned my repo, followed the same steps and got the same result.
He suspects that it has something to do with the variable classNames in my Button component with regards to Tailwind's JIT compiler, but none of us can pinpoint the error.
Am I using variable CSS classes wrong?
This has been a WTF moment. What could be the reason for this?

The CSS file generated by Tailwind will only include classes that it recognizes when it scans your code, which means that dynamically generated classes (e.g. col-span-${colSpan}) will not be included.
If you only need to span 2 columns, you could pass boolean values which will trigger the addition of a full col-span-2 or row-span-2 utility class to be added:
const Button = ({ colSpan = false, rowSpan = false, children }) => {
return (
<div
className={`${colSpan ? 'col-span-2' : ''} ${rowSpan ? 'row-span-2' : ''} bg-white p-3 rounded`}
>
<div className="flex items-center justify-center">{children}</div>
</div>
);
};
Otherwise, you could pass the values as classes to the Button component:
<Button className='col-span-2 row-span-1'>=</Button>
const Button = ({ className, children }) => {
return (
<div
className={`${className} bg-white p-3 rounded`}
>
<div className="flex items-center justify-center">{children}</div>
</div>
);
};
More information: https://tailwindcss.com/docs/content-configuration#dynamic-class-names

Another tricky solution that worked for me is to use variable with forced type of the possible className values (in typescript) like :
export type TTextSizeClass =
'text-xl' |
'text-2xl' |
'text-3xl' |
'text-4xl' |
'text-5xl' |
'text-6xl' |
'text-7xl' |
'text-8xl' |
'text-9xl'
;
...
const type : number = 6 ;
const textSizeClass : TTextSizeClass = type != 1 ? `text-${type}xl` : 'text-xl';
...
<div className={`font-semibold ${textSizeClass} ${className}`}>text</div>

As Ed Lucas said:
The CSS file generated by Tailwind will only include classes that it recognizes when it scans your code, which means that dynamically generated classes (e.g. col-span-${colSpan}) will not be included
But now could use safeListing
and
tailwind-safelist-generator package to "pregenerate" our dynamics styles.
With tailwind-safelist-generator, you can generate a safelist.txt file for your theme based on a set of patterns.
Tailwind's JIT mode scans your codebase for class names, and generates
CSS based on what it finds. If a class name is not listed explicitly,
like text-${error ? 'red' : 'green'}-500, Tailwind won't discover it.
To ensure these utilities are generated, you can maintain a file that
lists them explicitly, like a safelist.txt file in the root of your
project.

Related

Fetching users from Supabase results in /undefined user page

I am creating a directory of users in Next.js. The users are stored in Supabase. I need for all the users to be displayed in the index.js file, looping through them and showing them on a grid. This is working with getStaticProps, fetching the data and passing it as props profiles.
However, when clicking on each profile, it does redirect me to the [id].js page, but it appends /undefined to the url, rather than the id.
My file tree looks as follows:
pages
people
index.js
[id].js
export default function People({ profiles }) {
return (
<div className="body min-h-[90vh]">
<Head>
<title>People</title>
<link rel="icon" href="/logo" />
</Head>
<div
key={profiles.id}
profiles={profiles}
className="flex flex-col items-center py-16"
>
<div className="grid md:grid-flow-col md:grid-cols-3 xl:grid-cols-4 gap-8 lg:gap-12">
{profiles.map((profile) => (
<Link href={`/people/${profiles.id}`} key={profiles.id}>
<div
profile={profile}
id={profile.id}
className="flex flex-col w-full justify-center items-center p-8 shadow-md hover:shadow-lg"
>
{profile.avatar_url && (
<Image
src={profile.avatar_url}
alt="profile picture"
width={200}
height={200}
className="rounded-full"
object-fit="cover"
/>
)}
<h1 className="text-2xl pt-8 text-center">
{profile.full_name}
</h1>
<p>{profile.skills.skill}</p>
<button className="button w-full">See lessons</button>
</div>
</Link>
))}
</div>
</div>
</div>
);
}
export async function getStaticProps() {
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || "",
process.env.SUPABASE_SERVICE_ROLE_KEY || ""
);
const { data } = await supabaseAdmin
.from("profiles")
.select("*, skills(skill)")
.order("id");
console.log(data);
return {
props: {
profiles: data,
},
};
}
Any ideas as to what I am doing wrong are highly appreciated.
Thanks.

Popover on table rendering multiple instances in Vue3

I am trying to create a Vue component which wraps a Popoverfrom headlessui
I am trying to toggle open close using the slot binding as suggested in the popover docs. I cannot get this to work. Nothing appears in the UI, even if I had it working ok from within the element itself, granted that is neither a good place to put it because the elements render as many rows there are in the table.
Does anyone have any wisdom surrounding headless UI popovers in Vue3 on a table row?
EDIT:
Ok adding static to the PopoverPanelmakes it render when open=true
, the issue now being that multiple instances render based on the number of elements in the table. So this is a new issue.
<template>
<div>
<Popover :slot="{pop}">
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-1 opacity-0"
>
<PopoverPanel static class="absolute z-9 mt-3 max-w-sm -translate-x-1/2 transform sm:px-0 lg:max-w-3xl">
<div class="rounded-lg shadow-lg w-full m-2">
<div
v-if="pop"
ref="popover"
class="flex flex-col p-2"
>
<ButtonTemp label="Send reminder" btn-type="m-0.5 bg-neutral-200 text-neutral-900 text-left text-caption hover:bg-neutral-900 hover:text-neutral-100" icon-left>
<IconsMail />
</ButtonTemp>
<ButtonTemp label="Share candidate" btn-type="m-0.5 bg-neutral-200 text-neutral-900 text-caption hover:bg-neutral-900 hover:text-neutral-100" icon-left>
<IconsExternalLink />
</ButtonTemp>
<ButtonTemp label="Remove from assignment" btn-type="m-0.5 bg-neutral-200 text-neutral-900 text-caption hover:bg-danger-100 hover:text-neutral-100" icon-left>
<IconsUserMinus />
</ButtonTemp>
</div>
</div>
</PopoverPanel>
</transition>
</Popover>
<script setup lang="ts">
import { Ref } from '#vue/runtime-core';
import { Popover, PopoverButton, PopoverPanel } from '#headlessui/vue';
import { ButtonTemp, IconsMail, IconsExternalLink, IconsUserMinus } from '#/.nuxt/components';
const pop: Ref<boolean> = ref(true);
interface IContextMenu {
open: boolean;
}
const props = defineProps<IContextMenu>();
const isOpen = toRef(props, 'open');
watch(isOpen, (is) => {
if (is) {
console.log('open', is);
pop.value = !pop.value;
}
});
onMounted(() => {
pop.value = true;
});
</script>
Component where it is to be used:
......
<td class="group-hover:text-neutral-100 rounded-r">
<button
class="m-auto h-8 w-8 rounded-sm flex justify-center items-center group-hover:bg-neutral-700 cursor-pointer"
#click="handleClick(candidate.id, '')"
>
<ContextMenu :open="open" /> // Popover wrapped component
<IconsVerticalMenu />
</button>
</td>
</tr>

How to use variables to dynamic change TailwindCSS properties?

I'm building a portfolio and i'm currently stuck in a section where i'm looping thru an object which contains the respective color that I want to use (eg:
export const skills = [
{
id: 1,
name: "front-end",
Icon: FaReact,
color: "#61DAFB",
},
In the component mapped receiving these props, I already logged the color variable and it's logging correctly. But when I try to use that variable to dynamic change the color of the component, it doesn't work at all.
const SkillCard = ({ name, Icon, tools, color }) => {
console.log(`[${color}]`);
return (
<article
className={`bg-black text-gray-300 w-full hover:shadow-lg hover:shadow-gray-800 flex flex-col gap-4 p-8 rounded-lg grayscale hover:grayscale-0 duration-200 border-b-[${color}] `}
>
<Icon style={style} size={50} />
<h1 className="font-bold text-xl capitalize">{name}</h1>
<p>{tools}</p>
</article>
);
};
Here you can see that I'm trying to use the border-bottom property to change depending on the color contained in the array of objects, but I just couldn't find the solution.
I already tried chanching the value of the property, to contain the square brackets, but didn't work as well.
Update January 23:
const SkillCard = ({ skill }) => {
const { name, Icon, tools, color } = skill;
const style = { color };
return (
<article
style={{
borderBottomStyle: "solid",
borderBottomColor: color,
borderBottomWidth: "8px",
}}
className="bg-black text-gray-400 hover:text-white w-full hover:shadow-lg hover:shadow-gray-800 flex flex-col gap-4 p-8 rounded-lg grayscale hover:grayscale-0 duration-200"
>
<Icon style={style} size={50} />
<h1 className="font-bold text-xl capitalize">{name}</h1>
<p>{tools}</p>
</article>
);
};
Just added that color property as an inline style and now it works as intended. Not shure if it's best practice, but it's what I achieved so far.
It seems that this is because Tailwind need the full class name (complete unbroken strings) to assign the correct style, according to Tailwind document.
Live demo of the example: stackblitz
For example, perhaps try something like:
export const skills = [
{
id: 1,
name: "front-end",
Icon: FaReact,
borderBotttomColor: "border-b-[#61DAFB]",
},
Then apply to the component, perhaps also add a bottom border width such as border-b-4:
const SkillCard = ({ name, Icon, tools, borderBotttomColor }) => {
return (
<article
className={`${borderBotttomColor} border-b-4 bg-black text-gray-300 w-full hover:shadow-lg hover:shadow-gray-800 flex flex-col gap-4 p-8 rounded-lg grayscale hover:grayscale-0 duration-200`}
>
<Icon style={style} size={50} />
<h1 className="font-bold text-xl capitalize">{name}</h1>
<p>{tools}</p>
</article>
);
};
Alternatively, dynamic class names could be defined as safelist in Tailwind configuration, although it might not be suitable for this use case.

Tailwind css in React: how can I get grid-cols to render with needed width only, not equally-wide?

Using Tailwind CSS in React, I want to:
Have a two-column page. Column 1, which holds the "Menu" component, should take up 20% of the screen. Column 2, which holds the "Templates" component, should take up 80% of the screen.
Column 2, the "Templates" component, renders 4 dropdowns of varying width. I want each of the dropdowns to render in a column, but I want each column to be only as wide as it needs to be to display the dropdown. I don't want the columns to have equal width. My problem is that the columns are of equal width. How can I get each column width to only be as wide as the dropdown that gets rendered inside it?
In other words, I want the page to look like this:
[menu button] [Carrier Groups] [States] [Policy Types] [Statuses]
...instead of like this:
[menu button] [Carrier Groups] [States] [Policy Types] [Statuses]
To render the 2-column page I'm doing this in my App component. This works fine. In the parent div I have "grid-cols-5", and in the 2nd child div I have "col-span-4" to consume 80% of the page.
function Content(){
return (
<div className="w-screen h-screen grid grid-cols-5 gap-3">
<div>
<Menu />
</div>
<div className="col-span-4">
<Templates/>
</div>
</div>
);
}
function App() {
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<Content />
<ReactQueryDevtools />
</QueryClientProvider>
);
}
export default App;
This is the Templates component that renders in the 2nd child div. I am rendering 4 components in 4 columns. As mentioned each component renders a dropdown. The 4 columns are of equal width. Is there anything I can change so that the 4 columns are only as wide as needed to render each dropdown?
const Templates = () => {
return (
<form>
<h1 className="prose-2xl m-0">Templates</h1>
<div className='w-screen grid grid-cols-4 gap-3'>
<div>
<h2>Carrier Group</h2>
<CarrierGroups />
</div>
<div>
<h2>State</h2>
<States />
</div>
<div>
<h2>Policy Type</h2>
<PolicyTypes />
</div>
<div>
<h2>Status</h2>
<Statuses/>
</div>
</div>
</form>
);
}
export default Templates;
To create a two-column 20% / 80% layout I would do how you suggest. When using grid-cols-{n} you are creating an explicit grid and the columns will be equal widths.
When you need columns of varying widths you can create an implicit grid, the key class here being auto-cols-min. You also need to include a col-start-{n} class for each column that you want.
<div class="grid auto-cols-min gap-2 bg-zinc-500 p-5">
<div class="col-start-1 w-20 bg-zinc-100 p-5">col 1</div>
<div class="col-start-2 w-40 bg-zinc-100 p-5">col 2</div>
<div class="col-start-3 w-24 bg-zinc-100 p-5">col 3</div>
<div class="col-start-4 w-48 bg-zinc-100 p-5">col 4</div>
</div>
Demo of above: https://play.tailwindcss.com/gKRtn7vFgi?layout=horizontal
More info:
https://tailwindcss.com/docs/grid-auto-columns
https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-columns

Nextjs not Rendering Images Stored in Public Folder in Production Only

I'm building a personal website with Nextjs and I'm encountering a weird issue with the Public folder. Per the documentation, static images are to be put in the Public folder. This works perfectly for all images in my local development mode. However, as soon as I push this to production with Vercel, some images would fail to render (showing broken image). I noticed it seems to load jpg images fine but not png / svg. I don't use next-images module and just use a simple tag.
My file structure (simplified) is:
public
static
Melbourne.jpg
avatar.png
pages
index.jsx
In my index.jsx (also super simplied)
function Homepage({ data }) {
return (
<motion.div id='home' initial='initial' animate='enter' exit='exit'>
<section
id='intro'
className='bg-dark flex content-center flex-wrap p-40 justify-center text-center min-h-screen w-screen'>
<div className='flex flex-col justify-center'>
<motion.img
src='/static/avatar.png' <-- Works in Dev but not Prod
className='flex mx-auto rounded-full h-32 w-32'
variants={{
initial: { opacity: 0 },
enter: {
opacity: 1,
transition: {
ease: 'easeIn',
duration: 0.5,
delay: 0.75,
},
},
}}
/>
</div>
</section>
<section id='about' className='bg-light w-screen p-10'>
<div className='h-2 border-t-4 border-dark w-10/12 flex flex-column mx-auto mt-12' />
<div className='w-1/3 mx-auto relative bottom-12 bg-light'>
<h1 className='font-header text-center'>About Me</h1>
</div>
<div className='flex mx-auto mb-8 w-11/12 md:w-3/4 grid grid-cols-3 gap-6 lg:grid-cols-3'>
<div className='py-8 col-span-2'>
Some text here
</div>
<div className=''>
<img
className='rounded-full w-full h-full'
src='/static/Melbourne.jpg' <-- Works in Dev & Prod
alt='melbourne-skyline'
/>
</div>
</div>
</section>
</motion.div>
);
}
export default Homepage;
Please ignore any possible syntax error, everything works on my end and I deleted a lot of things to simplify this. The only issue I'm encountering is the tag.
Thanks in advance for your help!
You have to rebuild [ npm run build ] the project after adding new assets, then commit the changes.

Resources