Link

Provides a custom <NuxtLink> component to handle any kind of links within your application.

Examples

Basic

PropDefaultTypeDescription
label-stringThe label of the link.
activeClass-stringThe class to apply when the link is active.
inactiveClass-stringThe class to apply when the link is inactive.
exactfalsebooleanTrigger the link active class only on exact matches of the path.
exactQueryfalsebooleanTrigger the link active class only on exact matches of the query.
exactHashfalsebooleanTrigger the link active class only on exact matches of the hash.
active-booleanForce the link to be active regardless of the current route, applying the activeClass.
Preview
Code

Disabled

PropDefaultTypeDescription
disabledfalsebooleanWhether the link is disabled.
Preview
Code

Slots

NamePropsDescription
default-The content of the link.

Props

types/link.ts
import type { NuxtLinkProps } from 'nuxt/app'

export interface NLinkProps extends NuxtLinkProps {
  /**
   * The label of the link
   */
  label?: string
  /**
   * Manually enable/disable the exact match
   *
   * @default false
   */
  exact?: boolean
  /**
   * Manually enable/disable the exact match for the query string
   *
   * @default false
   */
  exactQuery?: boolean | 'partial'
  /**
   * Manually enable/disable the exact match for the hash
   *
   * @default false
   */
  exactHash?: boolean
  /**
   * Disable the link
   *
   * @default false
   */
  disabled?: boolean
  /**
   * Force the link to be active independent of the current route.
   *
   * @default false
   */
  active?: boolean
  /**
   * Active classes to apply when the link is inactive
   *
   * @example 'text-primary'
   */
  inactiveClass?: string
  /**
   * Useful in combination with `NavLink` to apply the active class to the parent element
   *
   */
  navLinkActive?: string

  /**
   * Useful in combination with `NavLink` to apply the inactive class to the parent element
   */
  navLinkInactive?: string
}

Presets

shortcuts/link.ts
type LinkPrefix = '_link'

export const staticLink: Record<`${LinkPrefix}-${string}`, string> = {
  // base
  '_link-disabled': 'n-disabled',
}

export const dynamicLink: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
]

export const link = [
  ...dynamicLink,
  staticLink,
]

Components

Link.vue
<script lang="ts">
import type { PropType } from 'vue'
import type { NLinkProps } from '../../types'
import { NuxtLink } from '#components'
import { useRoute } from '#imports'
import { diff, isEqual } from 'ohash/utils'
import { defineComponent } from 'vue'

export default defineComponent({
  inheritAttrs: false,
  props: {
    ...NuxtLink.props,
    // config
    label: {
      type: String as PropType<NLinkProps['label']>,
      default: undefined,
    },
    /** Force the link to be active independent of the current route. */
    active: {
      type: Boolean as PropType<NLinkProps['active']>,
      default: undefined,
    },
    exact: {
      type: Boolean as PropType<NLinkProps['exact']>,
      default: false,
    },
    exactQuery: {
      type: Boolean as PropType<NLinkProps['exactQuery']>,
      default: false,
    },
    exactHash: {
      type: Boolean as PropType<NLinkProps['exactHash']>,
      default: false,
    },
    disabled: {
      type: Boolean as PropType<NLinkProps['disabled']>,
      default: false,
    },
    // styling
    inactiveClass: {
      type: String as PropType<NLinkProps['inactiveClass']>,
      default: undefined,
    },

    // TODO: convert to sidebar preset
    navLinkActive: {
      type: String as PropType<NLinkProps['navLinkActive']>,
      default: undefined,
    },
    navLinkInactive: {
      type: String as PropType<NLinkProps['navLinkInactive']>,
      default: undefined,
    },
  },
  setup(props: any) {
    const route = useRoute()

    function resolveLinkClass(route: any, $route: any, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }): string | null {
      if (props.active === true) {
        return props.activeClass
      }

      if (props.active === false) {
        return props.inactiveClass
      }

      if (props.exactQuery && !isEqual(route.query, $route.query))
        return props.inactiveClass

      if (props.exactHash && !isEqual(route.hash, $route.hash))
        return props.inactiveClass

      if (props.exact && isExactActive)
        return props.exactActiveClass

      if (!props.exact && isActive)
        return props.activeClass

      return props.inactiveClass
    }

    function resolveNavLinkActive(route: any, $route: any, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }): string | null {
      if (props.exactQuery && !isEqual(route.query, $route.query))
        return null

      if (props.exactHash && !isEqual(route.hash, $route.hash))
        return null

      if (props.exact && isExactActive)
        return props.navLinkActive

      if (!props.exact && isActive)
        return props.navLinkActive

      return null
    }

    function resolveNavLinkInactive(route: any, $route: any, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }): string | null {
      if (props.exactQuery && !isEqual(route.query, $route.query))
        return props.navLinkInactive

      if (props.exactHash && !isEqual(route.hash, $route.hash))
        return props.navLinkInactive

      if ((!props.exact && isActive) || (props.exact && isExactActive))
        return null

      return props.navLinkInactive
    }

    function isPartiallyEqual(item1: any, item2: any) {
      const diffedKeys = diff(item1, item2).reduce((filtered: Set<string>, q: any) => {
        if (q.type === 'added') {
          filtered.add(q.key)
        }
        return filtered
      }, new Set<string>())

      const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
      const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))

      return isEqual(item1Filtered, item2Filtered)
    }

    function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
      if (props.active !== undefined) {
        return props.active
      }

      if (props.exactQuery === 'partial') {
        if (!isPartiallyEqual(linkRoute.query, route.query))
          return false
      }
      else if (props.exactQuery === true) {
        if (!isEqual(linkRoute.query, route.query))
          return false
      }

      if (props.exactHash && linkRoute.hash !== route.hash) {
        return false
      }

      if (props.exact && isExactActive) {
        return true
      }

      if (!props.exact && isActive) {
        return true
      }

      return false
    }

    return {
      resolveLinkClass,
      resolveNavLinkActive,
      resolveNavLinkInactive,
      isLinkActive,
      label: props.label,
      disabled: props.disabled,
    }
  },
})
</script>

<template>
  <NuxtLink
    v-slot="{ route, href, target, rel, navigate, isActive, isExactActive, isExternal }"
    v-bind="$props"
    custom
  >
    <a
      v-bind="$attrs"
      :href="disabled ? undefined : href"
      :rel="rel ?? undefined"
      :aria-disabled="disabled ? 'true' : undefined"
      :target="target ?? undefined"
      :class="[
        { '_link-disabled': disabled },
        resolveLinkClass(route, $route, { isActive, isExactActive }),
      ]"
      :nav-link-active="resolveNavLinkActive(route, $route, { isActive, isExactActive })"
      :nav-link-inactive="resolveNavLinkInactive(route, $route, { isActive, isExactActive })"
      @click="(e) => !isExternal && navigate(e)"
    >
      <slot :active="isLinkActive({ route, isActive, isExactActive })">
        {{ label }}
      </slot>
    </a>
  </NuxtLink>
</template>