Link
Overview
SLink is a polymorphic link component that supports internal routing (via RouterLink) and external links (via <a> tag). It automatically detects external links and sets safe target and rel attributes, supports disabled state and polymorphic rendering (as/asChild).
Features
- 🔗 Smart Routing: Automatically distinguishes internal routes (
to) from external links (href), gracefully falls back to<a>when RouterLink is unavailable. - 🧩 Pluggable Link Component: Inject a framework-specific component (e.g. Nuxt
NuxtLink) viaSConfigProviderlinkComponent. - 🛡️ Safe Defaults: External links automatically get
target="_blank"andrel="noopener noreferrer"to prevent reverse tabnabbing. - 🔧 Polymorphic: Render as any element or component via
as/asChild. - ♿ Accessible Disabled: Disabled state sets
aria-disabled,role="link",tabindex="-1", and blocks click events. - 🎨 Styling: Override styles via
classprop;linkVariantsprovides base utility classes.
Usage
Demos
Basic
Accessibility
- Disabled state: Sets
aria-disabled="true",role="link",tabindex="-1", and callspreventDefault()to block click navigation. Visually, thedata-disabledattribute triggerscursor-not-allowedandopacity-50. - External link safety: Automatically adds
rel="noopener noreferrer"to prevent newly opened pages from accessingwindow.opener.
API
Aria Layer
3Link
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
to | string | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | — | Route Location the link should navigate to when clicked on. |
replace | boolean | — | Calls `router.replace` instead of `router.push`. |
href | string | — | The URL the link should navigate to when clicked on. |
disabled | boolean | — | When `true`, the link is disabled. |
activeClass | string | — | Class to apply when the link is active |
exactActiveClass | string | — | Class to apply when the link is exact active |
inactiveClass | string | — | The class to apply to the link when it is inactive. |
prefetchedClass | string | — | A class to apply to links that have been prefetched. |
external | boolean | — | Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases |
ariaCurrentValue | 'true' | 'false' | 'page' | 'date' | 'time' | 'step' | 'location' | `'page'` | Value passed to the attribute `aria-current` when the link is exact active. |
viewTransition | boolean | — | Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported. |
target | (string & {}) | '_blank' | '_parent' | '_self' | '_top' | null | — | Where to display the linked URL, as the name for a browsing context. |
rel | (string & {}) | 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | null | 'noopener noreferrer' | A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links. |
noRel | boolean | — | If set to true, no rel attribute will be added to the link |
prefetch | boolean | — | When enabled will prefetch middleware, layouts and payloads of links in the viewport. |
prefetchOn | 'visibility' | 'interaction' | Partial<{ visibility: boolean; interaction: boolean; }> | — | Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility. |
noPrefetch | boolean | — | Escape hatch to disable `prefetch` attribute. |
trailingSlash | 'append' | 'remove' | — | An option to either add or remove trailing slashes in the `href` for this specific link.
Overrides the global `trailingSlash` option if provided. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
LinkBase
Props
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | The URL the link should navigate to when clicked on. |
to | string | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | — | Route Location the link should navigate to when clicked on. |
external | boolean | — | Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases |
target | (string & {}) | '_blank' | '_parent' | '_self' | '_top' | null | — | Where to display the linked URL, as the name for a browsing context. |
LinkExtra
Props
| Prop | Type | Default | Description |
|---|---|---|---|
replace | boolean | — | Calls `router.replace` instead of `router.push`. |
disabled | boolean | — | When `true`, the link is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
activeClass | string | — | Class to apply when the link is active |
exactActiveClass | string | — | Class to apply when the link is exact active |
inactiveClass | string | — | The class to apply to the link when it is inactive. |
prefetchedClass | string | — | A class to apply to links that have been prefetched. |
ariaCurrentValue | 'true' | 'false' | 'page' | 'date' | 'time' | 'step' | 'location' | `'page'` | Value passed to the attribute `aria-current` when the link is exact active. |
viewTransition | boolean | — | Pass the returned promise of `router.push()` to `document.startViewTransition()` if supported. |
rel | (string & {}) | 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | null | 'noopener noreferrer' | A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links. |
noRel | boolean | — | If set to true, no rel attribute will be added to the link |
prefetch | boolean | — | When enabled will prefetch middleware, layouts and payloads of links in the viewport. |
prefetchOn | 'visibility' | 'interaction' | Partial<{ visibility: boolean; interaction: boolean; }> | — | Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility. |
noPrefetch | boolean | — | Escape hatch to disable `prefetch` attribute. |
trailingSlash | 'append' | 'remove' | — | An option to either add or remove trailing slashes in the `href` for this specific link.
Overrides the global `trailingSlash` option if provided. |
Notes
Architecture and benchmark differences
VeanUI splits the link into an Aria layer (route detection, disabled handling, RouterLink integration) and a styled layer (linkVariants styling). This differs from single-package solutions like Nuxt Link, React Router Link, and Next.js Link.
| Aspect | VeanUI | Nuxt Link / React Router Link / Next.js Link |
|---|---|---|
| Architecture | Aria + styled split | single package |
| Route detection | auto-detect http prefix, external prop, RouterLink availability | explicit <NuxtLink> / <Link> |
| External links | auto target="_blank" + rel="noopener noreferrer" | manual setup required |
| Disabled state | aria-disabled + tabindex="-1" + preventDefault | manual handling required |
| Polymorphism | as/asChild via Primitive | as / passHref |
| Styling | UnoCSS utility classes + linkVariants | CSS Modules / styled / none |
FAQ
When to use to vs href? Use to for internal route navigation (requires RouterLink) and href for external links. When to is a string starting with http, the component automatically treats it as an external link and renders an <a> tag.
Can I use it without vue-router installed? Yes. The component uses resolveComponent('RouterLink') to check if RouterLink is available. When unavailable, it automatically falls back to rendering an <a> tag.
Can I plug in a framework-specific link component (Nuxt NuxtLink, a locale-prefixing Link)? Yes. Set linkComponent on SConfigProvider (same injection mechanism as iconRender); Link then renders internal links with that component instead of the global RouterLink. Framework-specific props that LinkProps does not declare (e.g. locale) fall through as attrs to the injected component, and you can type them via module augmentation of LinkProps. External links still render a native <a>.
How do I disable a link? Set disabled: true. The component sets aria-disabled="true", tabindex="-1", role="link", and calls preventDefault() to block click navigation. The data-disabled attribute is added to trigger cursor-not-allowed and opacity-50 visually.
What’s the difference between as and asChild?as specifies the root element tag (e.g., as="button" renders a <button>). asChild makes the component not render its own root element, instead forwarding attributes and events to its child (similar to Radix UI’s asChild pattern).
What are the default values for target and rel? External links default to target="_blank", rel="noopener noreferrer"; internal routes default to target="_self". Both can be overridden via props.