# Vue <RouterLink> with disabled state

Vue Router is awesome, but we all know that <RouterLink> should have a disabled state.

Today, I've built my own <AppLink> together with 🤖 Gemini (gemini-2.5-pro-exp-03-25), and you can have it as a treat.

It is awesome and it has everything you need:

  • disabled state.
  • aria-disabled="true" on an inactive link.
  • Optional tooltip with v-tippy! I was using props.iCanHasCheeseburger ? '' : 'opacity-20 pointer-events-none' with plain RouterLink before, but it was incompatible with v-tippy (it need events). So, I had to build this.

Enjoy!

import {
  defineComponent,
  h,
  type DefineComponent,
  type PropType,
  type HTMLAttributes,
  type VNodeProps,
  withDirectives,
  resolveDirective,
} from 'vue'
import { RouterLink, type RouterLinkProps } from 'vue-router'

// Combine RouterLinkProps with potential HTML attributes and Vue-specific props
type CombinedProps = RouterLinkProps & VNodeProps & HTMLAttributes;

type AppLinkProps = Omit<CombinedProps, 'disabled'> & {
  // Omit original HTML disabled, use ours
  /**
   * Whether the link is disabled. If true, renders a `<span>` instead of a link
   * and prevents navigation. Adds specific classes, aria-disabled attribute, and a tooltip.
   */
  disabled?: boolean;
  /**
   * Tooltip text to display when the link is disabled.
   */
  disabledTooltip?: string;
};

export default defineComponent({
  inheritAttrs: false, // Disable default attribute inheritance, handle manually
  props: {
    // @ts-expect-error It's okay, RouterLink props are included
    ...RouterLink.props,
    disabled: {
      type: Boolean as PropType<AppLinkProps['disabled']>,
      default: false,
    },
    disabledTooltip: {
      type: String as PropType<AppLinkProps['disabledTooltip']>,
      default: undefined,
    },
  },
  setup (props, { slots, attrs }) {
    return () => {
      if (props.disabled) {
        // Props for the disabled span
        const spanProps: HTMLAttributes & VNodeProps = {
          ...attrs, // Spread non-prop attributes first
          class: [attrs.class, 'cursor-default opacity-20'], // Merge classes
          'aria-disabled': true,
        }
        // Create the base span VNode
        const spanVNode = h('span', spanProps, slots)

        // Resolve the v-tippy directive
        const tippyDirective = resolveDirective('tippy')

        // Apply the directive if found
        if (tippyDirective && props.disabledTooltip) {
          return withDirectives(spanVNode, [
            [
              tippyDirective,
              { content: props.disabledTooltip, placement: 'top' },
            ],
          ])
        } else {
          return spanVNode // Return the span without the tooltip
        }
      } else {
        // Props for the active RouterLink
        // Type assertion needed as props are RouterLink specific + attrs
        const routerLinkProps = { ...attrs, ...props } as CombinedProps
        return h(RouterLink, routerLinkProps, slots)
      }
    }
  },
}) as DefineComponent<AppLinkProps>