forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { computed, type ComputedRef, type Ref, unref } from 'vue'
2import { useMutationObserver, useResizeObserver, useSupported } from '@vueuse/core'
3
4type CssVariableSource = HTMLElement | null | undefined | Ref<HTMLElement | null | undefined>
5
6type UseCssVariableOptions = {
7 element?: CssVariableSource
8 watchResize?: boolean
9 watchHtmlAttributes?: boolean
10}
11
12function readCssVariable(element: HTMLElement, variableName: string): string {
13 return getComputedStyle(element).getPropertyValue(variableName).trim()
14}
15
16function toCamelCase(cssVariable: string): string {
17 return cssVariable.replace(/^--/, '').replace(/-([a-z0-9])/gi, (_, c) => c.toUpperCase())
18}
19
20function resolveElement(element?: CssVariableSource): HTMLElement | null {
21 if (typeof window === 'undefined' || typeof document === 'undefined') return null
22 if (!element) return document.documentElement
23 const resolved = unref(element)
24 return resolved ?? document.documentElement
25}
26
27/**
28 * Read multiple CSS custom properties at once and expose them as a reactive object.
29 *
30 * Each CSS variable name is normalized into a camelCase key:
31 * - Leading `--` is removed
32 * - kebab-case is converted to camelCase
33 *
34 * Example:
35 * ```ts
36 * useCssVariables(['--bg', '--fg-subtle'])
37 * // => colors.value = { bg: '...', fgSubtle: '...' }
38 * ```
39 *
40 * The returned values are always resolved via `getComputedStyle`, meaning the
41 * effective value is returned (after cascade, theme classes, etc.).
42 *
43 * Reactivity behavior:
44 * - Updates automatically when the observed element changes
45 * - Can react to theme toggles via `watchHtmlAttributes`
46 * - Can react to responsive CSS variables via `watchResize`
47 *
48 * @param variables - List of CSS variable names (must include the leading `--`)
49 * @param options - Configuration options
50 * @param options.element - Element to read variables from (defaults to `:root`)
51 * @param options.watchResize - Re-evaluate values on resize (useful for media-query-driven variables)
52 * @param options.watchHtmlAttributes - Re-evaluate values when `<html>` attributes change
53 *
54 * @returns An object containing a reactive `colors` map, keyed by camelCase names
55 */
56export function useCssVariables(
57 variables: readonly string[],
58 options: UseCssVariableOptions = {},
59): { colors: ComputedRef<Record<string, string>> } {
60 const isClientSupported = useSupported(
61 () => typeof window !== 'undefined' && typeof document !== 'undefined',
62 )
63
64 const elementComputed = computed(() => resolveElement(options.element))
65
66 const colors = computed<Record<string, string>>(() => {
67 const element = elementComputed.value
68 if (!element) return {}
69
70 const result: Record<string, string> = {}
71 for (const variable of variables) {
72 result[toCamelCase(variable)] = readCssVariable(element, variable)
73 }
74 return result
75 })
76
77 if (options.watchResize) {
78 useResizeObserver(elementComputed, () => void colors.value)
79 }
80
81 if (options.watchHtmlAttributes && isClientSupported.value) {
82 useMutationObserver(document.documentElement, () => void colors.value, {
83 attributes: true,
84 attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'],
85 })
86 }
87
88 return { colors }
89}