Keyboard shortcut: Ctrl + K
Get started

Icon

One icon, from whichever source you have. The library ships its own drawings, so nothing here needs an icon font, and a resolver lets you wire in a third-party set instead.

Usage

vue
<script setup lang="ts">
import { VIcon } from 'vectis-ui'
import { search } from 'vectis-ui/icons'
</script>

<template>
  <VIcon :name="search" label="Search" />
</template>

Examples

Size

size is a number of pixels, the icon being square. Left out, it takes the size its context imposes, and falls back to 1em with no context.

vue

16

20

24

32

48

Small text, with a in it.

Larger text, with a in it.

A heading

<script setup lang="ts">
import { VButton, VIcon, VTypography } from 'vectis-ui'
import { notifications } from 'vectis-ui/icons'

const SIZES = [16, 20, 24, 32, 48]
</script>

<template>
  <div class="stack">
    <div class="row">
      <div v-for="size in SIZES" :key="size" class="cell">
        <VIcon :name="notifications" :size="size" />
        <VTypography variant="caption" tone="muted">{{ size }}</VTypography>
      </div>
    </div>

    <!-- Given no size, the icon takes the one its context imposes: a control sets one
         for everything drawn inside it, so these three need nothing of their own. -->
    <div class="row">
      <VButton :icon-start="notifications" size="sm">Small</VButton>
      <VButton :icon-start="notifications">Medium</VButton>
      <VButton :icon-start="notifications" size="lg">Large</VButton>
    </div>

    <!-- And with no context either, it falls back to 1em and follows the text it sits in. -->
    <div class="prose">
      <VTypography variant="body-sm">
        Small text, with a <VIcon :name="notifications" /> in it.
      </VTypography>
      <VTypography variant="body-lg">
        Larger text, with a <VIcon :name="notifications" /> in it.
      </VTypography>
      <VTypography variant="heading-2">A heading <VIcon :name="notifications" /></VTypography>
    </div>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-7);
}
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--vectis-space-5);
}
.cell {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
}
.prose {
  display: grid;
  gap: var(--vectis-space-2);
}
</style>

Filled

filled asks for the filled form of the icon, which is what marks a state. A library icon with no filled drawing renders the one it always had.

vue

notifications

check_circle

warning

error

info

schedule

search, unchanged by filling

<script setup lang="ts">
import { VIcon, VTypography } from 'vectis-ui'
import {
  check_circle as checkCircle,
  error,
  info,
  notifications,
  schedule,
  search,
  warning,
} from 'vectis-ui/icons'

/* Six of the icons whose geometry really changes when they are filled. */
const PAIRS = [notifications, checkCircle, warning, error, info, schedule]
</script>

<template>
  <div class="stack">
    <div class="row">
      <div v-for="icon in PAIRS" :key="icon.name" class="cell">
        <span class="pair">
          <VIcon :name="icon" :size="28" />
          <VIcon :name="icon" :size="28" filled />
        </span>
        <VTypography variant="caption" tone="muted">{{ icon.name }}</VTypography>
      </div>
    </div>

    <!-- An icon whose geometry filling does not change is drawn as it always was. The
         prop is inert rather than wrong, so it can be bound without checking first. -->
    <div class="cell start">
      <span class="pair">
        <VIcon :name="search" :size="28" />
        <VIcon :name="search" :size="28" filled />
      </span>
      <VTypography variant="caption" tone="muted">search, unchanged by filling</VTypography>
    </div>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-6);
}
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-6);
}
.cell {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
}
.start {
  justify-items: start;
}
.pair {
  display: inline-flex;
  gap: var(--vectis-space-3);
}
</style>

Mirrored

mirrored flips the icon in a right-to-left context, for a glyph that points at a physical direction: a "previous" chevron points left in English and right in Arabic. The direction is the one the browser computed, so a dir on any ancestor is enough. Most icons mean the same thing both ways and take no flip.

vue

ltr

rtl

<script setup lang="ts">
import { VIcon, VTypography } from 'vectis-ui'
import { arrow_right_alt as arrowRight, chevron_left as chevronLeft, search } from 'vectis-ui/icons'
</script>

<template>
  <div class="stack">
    <div v-for="dir in ['ltr', 'rtl']" :key="dir" :dir="dir" class="row">
      <VTypography variant="caption" tone="muted">{{ dir }}</VTypography>
      <VIcon :name="chevronLeft" :size="28" mirrored />
      <VIcon :name="arrowRight" :size="28" mirrored />
      <!-- A magnifier means the same thing in both directions, so it takes no flip. -->
      <VIcon :name="search" :size="28" />
    </div>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-4);
}
.row {
  display: flex;
  align-items: center;
  gap: var(--vectis-space-4);
}
</style>

Where the drawing comes from

The drawing is looked for in a fixed order, and the first source that answers is the one drawn:

  1. render, an explicit description of what to draw: SVG path data, a component, an image, or a font's own class.
  2. src, the address of an image.
  3. name, offered to your resolver first, then to the drawing a library icon carries, then to a ligature font.
  4. The default slot, an inline SVG, reached when none of render, src and name was given.
vue
render, as SVG path data
render, as a component
src, an image file
name, from the library
name, unresolved
an inline SVG, through the slot
<script setup lang="ts">
import { h } from 'vue'
import { VIcon } from 'vectis-ui'
import { ICON_VIEW_BOX, cloud_upload as cloudUpload, code, search } from 'vectis-ui/icons'

import firefoxLogo from '~/assets/img/firefox-browser-svg.svg'

/* A component icon, the shape an icon set such as Lucide ships: the contract is a
   single <svg> root. The drawing is borrowed from the library's own registry rather
   than redrawn here. */
const CodeIcon = () =>
  h('svg', { viewBox: ICON_VIEW_BOX, fill: 'currentColor' }, [h('path', { d: code.paths[0] })])
</script>

<template>
  <div class="grid">
    <!-- `render` wins over everything, and short-circuits the resolver with it. -->
    <figure>
      <VIcon :render="{ path: cloudUpload.paths[0] }" :size="28" />
      <figcaption>render, as SVG path data</figcaption>
    </figure>

    <figure>
      <VIcon :render="{ component: CodeIcon }" :size="28" />
      <figcaption>render, as a component</figcaption>
    </figure>

    <figure>
      <VIcon :src="firefoxLogo" :size="28" />
      <figcaption>src, an image file</figcaption>
    </figure>

    <!-- One of the library's own icons: a name travelling with its drawing. The
         resolver is still asked for that name first, and the drawing answers when
         nothing else does. -->
    <figure>
      <VIcon :name="search" :size="28" />
      <figcaption>name, from the library</figcaption>
    </figure>

    <!-- A plain string is only ever a name. Nothing in the registry answers to this
         one, so it is left to a ligature font, which this site does not load: what
         you see is the fallback, the name drawn as its own text. -->
    <figure>
      <VIcon name="translate" :size="28" />
      <figcaption>name, unresolved</figcaption>
    </figure>

    <!-- Last resort: the slot, reached when neither `name` nor `src` was given. -->
    <figure>
      <VIcon :size="28">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M12 4v16M4 12h16" stroke-linecap="round" />
        </svg>
      </VIcon>
      <figcaption>an inline SVG, through the slot</figcaption>
    </figure>
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--vectis-space-5);
}
figure {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
  margin: 0;
}
figcaption {
  color: var(--vectis-color-text-muted);
  font-size: var(--vectis-text-caption-size);
  text-align: center;
}
</style>

The order is the contract: your resolver comes before the library's own drawings, and one answering nothing for a given name hands it back rather than leaving a gap, which is what makes a partial mapping usable. A plain string is only ever a name, never an address. Wiring a set in, and the list of what the library ships, are both on Iconography.

API

Props

PropTypeDefault
namestring | BuiltinIconnone
Which icon to draw. A plain string is a name: it is offered to your resolver, then left to an icon font as a ligature. One of the library's own icons, imported from vectis-ui/icons, carries its drawing with it; the resolver is still asked first, and the drawing answers when nothing else does.
renderIconRendernone
An explicit description of what to draw, an image, a component, a path or a class, which wins over everything else. This is the route every icon prop of the library takes when it is given something other than a plain name.
srcstringnone
The address of an image to use as the icon. It wins over name.
sizenumber | stringnone
A size in pixels, as a number or a numeric string. Left out, or given something that is not a number, the icon takes the size its context imposes, a button setting one for the icons inside it, and failing that 1em, which makes it follow the surrounding text.
labelstringnone
What the icon means, for screen readers. Leaving it out marks the icon as decorative and hides it from them, which is right whenever the surrounding text already says what it says.
filledbooleanfalse
Draws the filled version of the icon. The built-in icons honour it wherever filling actually changes the drawing, and a ligature font always does. It means nothing for an image or an inline SVG, whose shape is fixed.
mirroredbooleanfalse
Flips the icon horizontally in a right-to-left context, for a glyph that points at a physical direction: a "previous" chevron points left in English and right in Arabic. Off by default, since most icons mean the same thing in both directions. The direction is the one the browser computed, so a dir on any ancestor is enough.

Slots

SlotType
default{}
An inline SVG, used when neither src nor name was given.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }