Skip to content

useRemToPixel Composable

javascript
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'

/**
 * Composable for converting rem values to pixels
 * Provides reactive rem-to-pixel conversion with automatic updates when root font size changes
 * Only use this in component setup() context
 */
export function useRemToPixel() {
  // Check if we're in a valid Vue component context
  const instance = getCurrentInstance()
  if (!instance) {
    console.warn('useRemToPixel: Not in component context, returning simple conversion functions')
    return {
      rootFontSize: ref(16),
      convertRemToPixels: remToPixels,
      convertMultipleRemToPixels: (remValues) => remValues.map(remToPixels),
      convertOffsetToPixels: (remOffset) => {
        if (!Array.isArray(remOffset) || remOffset.length !== 2) return [0, 0]
        return [remToPixels(remOffset[0]), remToPixels(remOffset[1])]
      },
      updateRootFontSize: () => {},
      setupMonitoring: () => {},
      cleanupMonitoring: () => {}
    }
  }
  const rootFontSize = ref(16) // Default fallback

  /**
   * Convert rem value to pixels
   * @param {number} remValue - The rem value to convert
   * @returns {number} - The equivalent pixel value
   */
  const convertRemToPixels = (remValue) => {
    if (typeof remValue !== 'number' || isNaN(remValue)) {
      return 0
    }
    return remValue * rootFontSize.value
  }

  /**
   * Update the root font size from the DOM
   */
  const updateRootFontSize = () => {
    try {
      const rootElement = document.documentElement
      const computedStyle = getComputedStyle(rootElement)
      const fontSize = parseFloat(computedStyle.fontSize)
      
      if (!isNaN(fontSize) && fontSize > 0) {
        rootFontSize.value = fontSize
      }
    } catch (e) {
      console.warn('Failed to get root font size, using fallback:', e)
      rootFontSize.value = 16
    }
  }

  /**
   * Convert multiple rem values to pixels
   * @param {number[]} remValues - Array of rem values
   * @returns {number[]} - Array of pixel values
   */
  const convertMultipleRemToPixels = (remValues) => {
    return remValues.map(convertRemToPixels)
  }

  /**
   * Convert rem offset array [x, y] to pixel offset
   * @param {number[]} remOffset - [x, y] offset in rem
   * @returns {number[]} - [x, y] offset in pixels
   */
  const convertOffsetToPixels = (remOffset) => {
    if (!Array.isArray(remOffset) || remOffset.length !== 2) {
      return [0, 0]
    }
    return [
      convertRemToPixels(remOffset[0]),
      convertRemToPixels(remOffset[1])
    ]
  }

  // Set up font size monitoring only if we're in a component context
  let resizeObserver = null
  let isSetup = false

  const setupMonitoring = () => {
    if (isSetup) return
    isSetup = true

    updateRootFontSize()

    // Monitor for font size changes using ResizeObserver if available
    if (typeof ResizeObserver !== 'undefined') {
      resizeObserver = new ResizeObserver(() => {
        updateRootFontSize()
      })
      resizeObserver.observe(document.documentElement)
    }

    // Fallback: listen for window resize events
    window.addEventListener('resize', updateRootFontSize)
  }

  const cleanupMonitoring = () => {
    if (resizeObserver) {
      resizeObserver.disconnect()
      resizeObserver = null
    }
    window.removeEventListener('resize', updateRootFontSize)
    isSetup = false
  }

  // Set up lifecycle hooks (we already checked instance above)
  onMounted(() => {
    setupMonitoring()
  })

  onUnmounted(() => {
    cleanupMonitoring()
  })

  return {
    rootFontSize,
    convertRemToPixels,
    convertMultipleRemToPixels,
    convertOffsetToPixels,
    updateRootFontSize,
    setupMonitoring,
    cleanupMonitoring
  }
}

/**
 * Simple utility function for one-off rem to pixel conversions
 * Use this when you don't need reactive updates
 * @param {number} remValue - The rem value to convert
 * @returns {number} - The equivalent pixel value
 */
export function remToPixels(remValue) {
  if (typeof remValue !== 'number' || isNaN(remValue)) {
    return 0
  }
  
  try {
    const rootElement = document.documentElement
    const computedStyle = getComputedStyle(rootElement)
    const rootFontSize = parseFloat(computedStyle.fontSize)
    
    if (isNaN(rootFontSize) || rootFontSize <= 0) {
      return remValue * 16 // fallback to 16px
    }
    
    return remValue * rootFontSize
  } catch (e) {
    console.warn('Failed to convert rem to pixels, using 16px fallback:', e)
    return remValue * 16
  }
}