๐ŸŸก NavLink

Work in progress - this component is not yet ready for use.

Basic

NNavLink is a component that renders a link to a page in your application. It is a wrapper around the NButton and NuxtLink component that adds some additional functionality.

refer to the NButton and NuxtLink documentation for more information on the props that can be passed to those components.

<script setup lang="ts">
const links = [
  {
    label: 'Dashboard',
    to: '/navigation/nav-link',
  },
  {
    label: 'Profile',
    to: '/navigation/nav-link-group',
  },
  {
    label: 'Settings',
    to: '/navigation/breadcrumb',
  },
]
</script>

<template>
  <div class="flex flex-wrap gap-4">
    <NNavLink
      v-for="(link, i) in links"
      :key="i"
      v-bind="link"
    />
  </div>
</template>

Variants & Colors

nav-link-{variant}-{color} - add a variant to the navigation. The default variant is text-primary.

nav-link-active-{variant}-{color} - add a variant to the navigation when the link is active. The default variant is text-primary.

nav-link-inactive-{variant}-{color} - add a variant to the navigation when the link is inactive. The default variant is text-gray.

VariantDescription
textThe text variant (default)
solidThe solid variant.
~The unstyle or base variant
You can wrap the nav-link component with a dark class to force the component to use the dark variant always.
<script setup lang="ts">
const links = [
{
  label: 'Dashboard',
  to: '/navigation/nav-link',
},
{
  label: 'Profile',
  to: '/navigation/nav-link-group',
},
{
  label: 'Settings',
  to: '/navigation/breadcrumb',
},
]
</script>

<template>
<div class="flex flex-col gap-2">
  <!-- force dark mode always using `.dark` -->
  <div class="dark flex flex-wrap gap-4 rounded bg-muted p-3">
    <NNavLink
      v-for="(link, i) in links"
      :key="i" v-bind="link"
      nav-link="text-gray"
      nav-link-active="text-gray"
      nav-link-inactive="text-gray"
    />
  </div>

  <hr class="border-base">

  <div class="flex flex-wrap gap-4 rounded bg-primary p-3">
    <NNavLink
      v-for="(link, i) in links"
      :key="i" v-bind="link"
      nav-link="solid"
      nav-link-active="solid"
      nav-link-inactive="solid"
    />
  </div>
</div>
</template>

Props

import type { NBadgeProps } from './badge'
import type { NButtonProps } from './button'

export interface NNavLinkProps extends Omit<NButtonProps, 'una'> {
  navLink?: string
  navLinkInactive?: string
  navLinkActive?: string
  badge?: NBadgeProps

  una?: {
    navLinkDefaultVariant?: string
    navLink?: string
    navLinkActive?: string
    navLinkInactive?: string
  } & NButtonProps['una']
}

Presets

type NavLinkPrefix = 'nav-link'

export const staticNavLink: Record<`${NavLinkPrefix}-${string}` | NavLinkPrefix, string> = {
  // config
  'nav-link-default-variant': 'nav-link-text-primary',

  // base
  'nav-link': 'transition-base leading-6 justify-start gap-x-3 rounded-md',

  // badge
  'nav-link-badge': 'min-w-max whitespace-nowrap rounded-full px-2.5 py-.5 leading-5',

  // text-variant
  'nav-link-text-gray': 'hover:bg-$c-gray-100 hover:text-$c-gray-950',
  'nav-link-active-text-gray': 'bg-$c-gray-100 text-$c-gray-950',
  'nav-link-inactive-text-gray': 'text-$c-gray-950',
}

export const dynamicNavLink: [RegExp, (params: RegExpExecArray) => string][] = [
  // text-variant
  [/^nav-link-active-text(-(\S+))?$/, ([, , c = 'primary']) => `bg-$c-gray-100 text-${c}-600 dark:text-${c}-500`],
  [/^nav-link-inactive-text(-(\S+))?$/, ([, , c = 'gray']) => `text-${c}-800 dark:text-${c}-100`],
  [/^nav-link-text(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} hover:nav-link-active-text-${c}`],

  // solid-variant
  [/^nav-link-active-solid(-(\S+))?$/, ([, , c = 'primary']) => `bg-${c}-700 dark:bg-${c}-400 text-white dark:text-${c}-950`],
  [/^nav-link-inactive-solid(-(\S+))?$/, ([, , c = 'primary']) => `text-white dark:text-${c}-950`],
  [/^nav-link-solid(-(\S+))?$/, ([, , c = 'primary']) => `btn-focus-${c} hover:nav-link-active-solid-${c}`],
]

export const navLink = [
  ...dynamicNavLink,
  staticNavLink,
]

Component

<script setup lang="ts">
import { computed } from 'vue'
import type { NNavLinkProps } from '../../types'
import { omitProps } from '../../utils'
import NButton from '../elements/Button.vue'
import NIcon from '../elements/Icon.vue'

defineOptions({
  inheritAttrs: false,
})

const props = withDefaults(
  defineProps<NNavLinkProps>(),
  {
    navLinkActive: 'text-primary',
    navLinkInactive: 'text-primary',
    una: () => ({
      btnDefaultVariant: '~',
      navLinkDefaultVariant: 'nav-link-default-variant',
    }),
  },
)

const btnProps = omitProps(props.una, [
  'navLinkDefaultVariant',
  'navLink',
  'navLinkActive',
  'navLinkInactive',
])

const navLinkVariants = ['text', 'solid'] as const
const hasVariant = computed(() => navLinkVariants.some(navLinkVariants => props.navLink?.includes(navLinkVariants)))
const isBaseVariant = computed(() => props.navLink?.includes('~') || props.una.navLink?.includes('~'))
</script>

<template>
  <NButton
    :nav-link="navLink"
    :nav-link-active="navLinkActive"
    :nav-link-inactive="navLinkInactive"
    :una="btnProps"
    class="nav-link"
    :class="[
      !hasVariant && !isBaseVariant ? una?.navLinkDefaultVariant : null,
      { 'btn-reverse': reverse },
    ]"
    v-bind="{
      ...omitProps(props, ['badge', 'una']),
      ...$attrs,
    }"
  >
    <template #leading>
      <NIcon
        v-if="leading"
        :name="leading"
        :class="una?.btnLeading"
        btn="leading"
      />
    </template>

    <template #default>
      <span
        btn="label"
        class="w-full text-left"
        :class="una?.btnLabel"
      >
        {{ label }}
      </span>
    </template>

    <template v-if="badge" #trailing>
      <!-- TODO: move to una preset -->
      <NBadge
        v-bind="badge"
        :una="{
          badgeDefaultVariant: 'badge-outline-white',
          ...props.badge?.una,
        }"
        class="min-w-max whitespace-nowrap rounded-full px-2.5 py-0.5 leading-5"
      />
    </template>
  </NButton>
</template>