DEV Community

Mykolas Mankevicius
Mykolas Mankevicius

Posted on • Edited on

Tailwind Icon Mask Plugin for phoenix liveview

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'
  }
}
Enter fullscreen mode Exit fullscreen mode

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 })
    }),
  ]
}
Enter fullscreen mode Exit fullscreen mode

The plugin does a few things:

  1. 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 become stroke- and so forth.
  2. it supports the custom- prefix icons that are in your ./svg/icons/custom directory
  3. it also supports dynamic values to change the stroke-width, for e.g: custom-[ship,2] will produce and icon custom-ship with the stroke-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 like custom-[ship,sw:2,lc:y] and then parse each modifier to adjust as you see fit.
  4. you can also provide a name transform, for e.g. my custom icons have a prefix custom- so the full path is something like svg/icons/custom/custom-ship.svg if you wouldn't provide a name transformer you would need to type custom-custom-ship as the match component takes the first part as the custom: as a prefix for the value.
  5. Allows to easily add other component libraries. Change the path and the prefix matchComponents({ custom: (value: string | Icon) the prefix is the custom name in this part, you could have feather/hero or whatever you'd like.

Top comments (0)