Here's an update to this article How to integrate Tabler Icons into your Phoenix project
The article goes over the general implementation and you should read it to understand what is happening.
In this post I'm simply providing and improved solution after running into some bugs.
TL;DR; here's the helper:
import { readFileSync, readdirSync } from 'fs'
import { basename, join } from 'path'
import type { KeyValuePair } from 'tailwindcss/types/config'
export type Icon = {
name: string
fullPath: string
}
export type IconValues = KeyValuePair<string, Icon>
export const getIconValues = (iconsDir: string, transformName?: (name: string) => string) => {
const values: IconValues = {}
for (const file of readdirSync(iconsDir)) {
// Skip non-SVG files
if (!file.endsWith('.svg')) continue
const fullName = basename(file, '.svg')
const name = transformName ? transformName(fullName) : fullName
values[name] = { name, fullPath: join(iconsDir, file) }
}
return values
}
export const getIconCSS = (value: string | Icon, values: IconValues) => {
let name = ''
let fullPath = ''
let strokeWidth = '1.5'
if (typeof value === 'string') {
const hasModifier = value.includes(',')
const iconName = hasModifier ? value.split(',')[0] : value
const customStrokeWidth = hasModifier ? value.split(',')[1] : '1.5'
const icon = values[iconName] || {}
name = iconName
fullPath = icon.fullPath
strokeWidth = customStrokeWidth
} else {
name = value.name
fullPath = value.fullPath
}
if (!fullPath) {
return {}
}
const content = readFileSync(fullPath)
.toString()
.replace(/\r?\n|\r/g, '')
.replace(/<svg([^>]*)>/g, (_match, attributes: string) => {
// Remove width and height attributes (with preceding whitespace) from the svg opening tag, and no where else
const cleanedAttributes = attributes.replace(/\s+width="[^"]*"/g, '').replace(/\s+height="[^"]*"/g, '')
return `<svg${cleanedAttributes}>`
})
.replace(/\sstroke-width="[^"]*"/, ` stroke-width="${strokeWidth}"`)
const varName = `--icon-url-${name}`
return {
[varName]: `url('data:image/svg+xml;utf8,${content}')`,
'-webkit-mask': `var(${varName})`,
mask: `var(${varName})`,
'mask-repeat': 'no-repeat',
'background-color': 'currentColor',
'vertical-align': 'middle',
'horizontal-align': 'middle',
display: 'inline-block',
width: '1.25rem',
height: '1.25rem'
}
}
and here is how to use it:
import plugin from 'tailwindcss/plugin'
import { getIconValues, getIconCSS } from './plugin-icons-utils'
import { join } from 'path'
import type { IconValues, Icon } from './plugin-icons-utils'
module.exports = {
content: [
'./ts/**/*.{js,ts}',
],
plugins: [
plugin(({ matchComponents }) => {
const path = './svg/icons/custom'
const values: IconValues = getIconValues(join(__dirname, path), (key) => key.replace('custom-', ''))
matchComponents({ custom: (value: string | Icon) => getIconCSS(value, values) }, { values })
}),
]
}
The plugin does a few things:
- First off it fixes a bug where width and height, were being removed from everything inside the
.svg
content. For e.g.stroke-width
would becomestroke-
and so forth. - it supports the
custom-
prefix icons that are in your./svg/icons/custom
directory - it also supports dynamic values to change the stroke-width, for e.g:
custom-[ship,2]
will produce and iconcustom-ship
with thestroke-width: 2
this is a trick to provide masks with some customisation, you could come up with your own syntax if you need more changes, something likecustom-[ship,sw:2,lc:y]
and then parse each modifier to adjust as you see fit. - you can also provide a name transform, for e.g. my custom icons have a prefix
custom-
so the full path is something likesvg/icons/custom/custom-ship.svg
if you wouldn't provide a name transformer you would need to typecustom-custom-ship
as the match component takes the first part as thecustom:
as a prefix for the value. - Allows to easily add other component libraries. Change the
path
and the prefixmatchComponents({ custom: (value: string | Icon)
the prefix is thecustom
name in this part, you could have feather/hero or whatever you'd like.
Top comments (0)