Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@iconify/tailwindcss redundant code #290

Closed
sillvva opened this issue Mar 5, 2024 · 16 comments
Closed

@iconify/tailwindcss redundant code #290

sillvva opened this issue Mar 5, 2024 · 16 comments

Comments

@sillvva
Copy link
Contributor

sillvva commented Mar 5, 2024

When I hover over the class icon-[mdi--magnify], I see the following:

.icon-\[mdi--magnify\] {
  display: inline-block;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  -webkit-mask-image: var(--svg);
  mask-image: var(--svg);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5l-1.5 1.5l-5-5v-.79l-.27-.27A6.516 6.516 0 0 1 9.5 16A6.5 6.5 0 0 1 3 9.5A6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14S14 12 14 9.5S12 5 9.5 5'/%3E%3C/svg%3E");
}

Everything but the variable is redundant from icon to icon, and should probably be in a separate class to avoid repeated code in the final bundle. If you had 50 different icons on a page, it would have 50 instances of those CSS properties.

Instead of doing icon-[mdi--magnify], what about icon mdi-[magnify]?

/* component class */
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  -webkit-mask-image: var(--svg);
  mask-image: var(--svg);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

/* utility class */
.mdi-\[magnify\]
  --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5l-1.5 1.5l-5-5v-.79l-.27-.27A6.516 6.516 0 0 1 9.5 16A6.5 6.5 0 0 1 3 9.5A6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14S14 12 14 9.5S12 5 9.5 5'/%3E%3C/svg%3E");
}

For now, I'm doing this manually in my tailwind config file

import { icons as mdi } from "@iconify-json/mdi";

plugin(function ({ addComponents, addUtilities, theme }) {
	addComponents({
		".iconify": {
			display: "inline-block",
			width: theme("width.4"),
			height: theme("height.4"),
			backgroundColor: theme("colors.current"),
			maskImage: "var(--svg)",
			maskRepeat: "no-repeat",
			maskSize: "100% 100%"
		}
	});

	const mdiIcons = Object.entries(mdi.icons);
	for (const [name, icon] of mdiIcons) {
		const path = encodeURIComponent(icon.body.replace(` fill="currentColor"`, "")).replace(/%20/g, " ").replace(/%22/g, "'");
		addUtilities({
			[`.mdi-${name}`]: {
				"--svg": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E${path}%3C/svg%3E")`
			}
		});
	}
})
@cyberalien
Copy link
Member

There are 2 types of icons: monotone icons that are rendered as masks, icons with palette that are rendered as background. That makes common class name impossible.

@sillvva
Copy link
Contributor Author

sillvva commented Apr 28, 2024

There are 2 types of icons: monotone icons that are rendered as masks, icons with palette that are rendered as background. That makes common class name impossible.

Looked at the source for how the CSS is generated.
https://github.com/iconify/iconify/blob/main/packages/utils/src/css/common.ts#L14

Doesn't seem all that impossible to me.

import { icons as mdi } from "@iconify-json/mdi";
import { icons as logos } from "@iconify-json/logos";

plugin(function ({ addComponents, addUtilities, theme }) {
	// Uses text color
	addComponents({
		".iconify-mask": {
			display: "inline-block",
			width: theme("width.4"),
			height: theme("height.4"),
			backgroundColor: theme("colors.current"),
			maskImage: "var(--svg)",
			maskRepeat: "no-repeat",
			maskSize: "100% 100%"
		}
	});

	// Uses icon colors
	addComponents({
		".iconify-bg": {
			display: "inline-block",
			width: theme("width.4"),
			height: theme("height.4"),
			backgroundImage: "var(--svg)",
			backgroundRepeat: "no-repeat",
			backgroundSize: "100% 100%"
		}
	});

	const iconSets = {
		mdi: mdi.icons,
		logos: logos.icons
	};
	for (const [set, icons] of Object.entries(iconSets)) {
		for (const [name, icon] of Object.entries(icons)) {
			const path = encodeURIComponent(icon.body).replace(/%20/g, " ").replace(/%22/g, "'");
			const width = icon.width || 24;
			const height = icon.height || 24;
			addUtilities({
				[`.${set}-${name}`]: {
					"--svg": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${width} ${height}'%3E${path}%3C/svg%3E")`
				}
			});
		}
	}
})

@cyberalien
Copy link
Member

Good point. Developer needs to know which icon is used as background and which icon is used as mask, but that would be up to developer.

@cyberalien
Copy link
Member

Can probably make a new plugin module to work like that. I'll play with it...

@sillvva
Copy link
Contributor Author

sillvva commented Apr 28, 2024

Could just make the mask one the default and call the other .iconify-color

One issue I found with @iconify-json is that some icons are missing width or height. Like in @iconify-json/logos, discord-icon and google-icon are missing width.

@sillvva
Copy link
Contributor Author

sillvva commented May 6, 2024

I managed it with this.

Pulling data from getIconData filled in the missing heights and widths from @iconify-json

// tailwind.config.ts
import { icons as logos } from "@iconify-json/logos";
import { icons as mdi } from "@iconify-json/mdi";
import { getIconData } from "@iconify/utils";
import type { Config } from "tailwindcss";
import plugin from "tailwindcss/plugin";

type IconifyJSON = { icons: Record<string, { body: string; width?: number; height?: number }>; prefix: string };
function twIconifyPlugin(iconSets: Record<string, IconifyJSON>) {
	return plugin(function ({ addComponents, addUtilities, theme }) {
		addComponents({
			".iconify": {
				display: "inline-block",
				width: theme("width.4"),
				height: theme("height.4"),
				backgroundColor: theme("colors.current"),
				maskImage: "var(--svg)",
				maskRepeat: "no-repeat",
				maskSize: "100% 100%"
			}
		});

		addComponents({
			".iconify-color": {
				display: "inline-block",
				width: theme("width.4"),
				height: theme("height.4"),
				backgroundImage: "var(--svg)",
				backgroundRepeat: "no-repeat",
				backgroundSize: "100% 100%"
			}
		});

		const utilities: CSSRuleObject[] = [];
		for (const [set, dataset] of Object.entries(iconSets)) {
			for (let [name, icon] of Object.entries(dataset.icons)) {
				if ((icon.height && !icon.width) || (icon.width && !icon.height)) icon = getIconData(dataset, name) || icon;
				const path = encodeURIComponent(icon.body);
				const width = icon.width || 24;
				const height = icon.height || 24;
				utilities.push({
					[`.${set}-${name}`]: {
						"--svg": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${width} ${height}'%3E${path}%3C/svg%3E")`
					}
				});
			}
		}

		addUtilities(utilities);
	});
}

export default {
	content: ["./src/**/*.{html,js,svelte,ts}"],
	...
	plugins: [
		twIconifyPlugin({ mdi, logos })
	],
	...
} satisfies Config;

With that, I can use iconify-color logos-google-icon and get the Google icon in color. I can also use iconify mdi-logout to get the logout icon from Material Design Icons matching the text color.

And with this function, all you need to do is pass in the icon sets you want to use.

@cyberalien
Copy link
Member

cyberalien commented May 10, 2024

Added plugin in dev/tailwind branch. Will do more testing before publishing it.

It adds new plugin addIconSelectors that works similar to requested.

Parameters can be:

  • Array of prefixes.
  • More advanced parameters, which also require array of prefixes in prefixes property.

Basic usage:

addIconSelectors(['mdi'])

This will create selectors .iconify for monotone icons, .iconify-color for colored icons, .mdi--{name} (note double dash between prefix and name) for all icons in mdi icon set.

Advanced usage:

addIconSelectors({
  prefixes: ['mdi'], // prefixes
  maskSelector: '.iconify-color', // customise mask selector
  backgroundSelector: '.iconify', // customise background selector
  iconSelector: '.{prefix}--{name}', // customise icon selector, must have "{prefix}" and "{name}" in it
  varName: 'svg', // variable name to use
  scale: 1, // scale icons, which sets width/height in background/mask selectors
  extraMaskRules: {}, // extra rules to add to mask selector
  extraBackgroundRules: {}, // extra rules to add to background selector
  customise: (content, name, prefix) => content, // callback to customise icons. First param is content, second is icon name, third is icon set prefix
})

Furthermore, prefixes list accepts not only strings, but custom icon sets. I'll add more details about that later, demo in dev branch has usage example.

@sillvva
Copy link
Contributor Author

sillvva commented May 10, 2024

That looks amazing! I like the idea of being able to customize class and variable names as they could conflict.

Though I think your example has the maskSelector and backgroundSelector backwards.

@cyberalien
Copy link
Member

Thanks!

And yes, example has those selectors backwards 😄

Published version 1.1.0 with changes.

Documentation has also been rewritten: https://iconify.design/docs/usage/css/tailwind/iconify/

@sillvva
Copy link
Contributor Author

sillvva commented May 13, 2024

It mostly works! One issue is that the .iconify and .iconify-color selectors are being created as utility classes when they should be component classes. This means the built-in width and height properties, for example, override the w-# and h-# utility classes. This specificity override is due to the way Tailwind orders the component and utility classes in the build output.

@cyberalien
Copy link
Member

Set scale option to 0 to remove width/height. See https://iconify.design/docs/usage/css/tailwind/iconify/size-color.html#custom

You can also use extraMaskRules and extraBackgroundRules to add custom rules.

@sillvva
Copy link
Contributor Author

sillvva commented May 13, 2024

True, but that method forces the dev to specify w/h manually for all icons, because it removes the default. The whole point of component classes is that you can override the defaults in them with utility classes on a case-by-case basis. Using the config doesn't allow modification on a case-by-case basis.

As such, I've created a PR that should work.

@cyberalien
Copy link
Member

Hm. Can you please explain the difference? I have very limited experience with Tailwind, not using it in any of my own projects.

How would customisation work that is not available with current code, how will it override default values?

@sillvva
Copy link
Contributor Author

sillvva commented May 13, 2024

It has to do with ordering of selectors that otherwise have the same specificity. .iconify and .w-6 both have the same specificity. So if .iconify comes later in the Tailwind build, it will override the styles of .w-6. You would have to use .!w-6 to add !important to force it, which is not desirable.

Tailwind compiles component classes before utility classes, allowing utility classes to override component classes on a case-by-case basis.

@cyberalien
Copy link
Member

Thanks! That makes perfect sense.

I'll merge and republish it as 1.1.1, though with few changes - will split code that generates reusable selectors and icons, so they could be called separately instead of destructuring result.

@cyberalien
Copy link
Member

Tested, it works correctly.

Published version 1.1.1 with fix.

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants