Raccourci clavier : Ctrl + K
Commencer

Champ de date

Un champ de texte saisissable, avec un VDatePicker dans un panneau à côté. Le masque suit la langue : l'ordre des champs, le séparateur et le gabarit sont tous dérivés de la locale.

Utilisation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const date = ref<string | null>(null)
</script>

<template>
  <VDateInput v-model="date" label="Start date" show-picker />
</template>

Exemples

Libellé, aide et icône

label et hint se comportent comme sur n'importe quel champ. pickerIcon change le glyphe qui ouvre le calendrier, iconStart pose une icône au début du champ, et loading affiche un indicateur à la place de l'icône de calendrier. pickerIconLabel, clearLabel, loadingText et iconStartLabel renomment ce que chacun annonce.

vue

Type it or pick it from the calendar

The calendar is the only way in

Chargement…

An icon at the start, a spinner at the end while something loads

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'
import { calendar_today as calendarToday, schedule, search } from 'vectis-ui/icons'

const start = ref<string | null>('2026-06-10')
const deadline = ref<string | null>(null)
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="start"
      label="Start date"
      hint="Type it or pick it from the calendar"
      show-picker
      :picker-icon="calendarToday"
    />

    <VDateInput
      v-model="deadline"
      label="Deadline"
      hint="The calendar is the only way in"
      mode="picker"
      :picker-icon="schedule"
    />

    <!-- The start icon is rendered before whatever else fills that end of the field. -->
    <VDateInput
      v-model="start"
      label="Filter by date"
      hint="An icon at the start, a spinner at the end while something loads"
      :icon-start="search"
      loading
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Tailles

size définit la hauteur du champ à 32, 40 ou 48 pixels, et compact lui retire 4px. Le panneau garde ses propres mesures.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const rows = ref(
  (['sm', 'md', 'lg'] as const).flatMap((size) => [
    { key: size, size, compact: false, label: size, date: '2026-06-10' as string | null },
    {
      key: `${size}-compact`,
      size,
      compact: true,
      label: `${size}, compact`,
      date: '2026-06-10' as string | null,
    },
  ]),
)
</script>

<template>
  <div class="column">
    <VDateInput
      v-for="row in rows"
      :key="row.key"
      v-model="row.date"
      :size="row.size"
      :compact="row.compact"
      :label="row.label"
      show-picker
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Modes

mode choisit la façon de renseigner la valeur : input masque le champ pour n'y saisir que des chiffres, le calendrier devenant alors optionnel via showPicker ; picker fait du calendrier la seule entrée. La saisie est réservée à une date unique.

vue

No icon, no panel: the field is the whole control

showPicker adds the icon and opens the panel on focus

Nothing can be typed, so the calendar is the only way in

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const typed = ref<string | null>('2026-06-10')
const withPicker = ref<string | null>('2026-06-10')
const readOnly = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="typed"
      label="Typed, the default"
      hint="No icon, no panel: the field is the whole control"
    />

    <VDateInput
      v-model="withPicker"
      show-picker
      label="Typed, with the calendar"
      hint="showPicker adds the icon and opens the panel on focus"
    />

    <VDateInput
      v-model="readOnly"
      mode="picker"
      label="Read-only"
      hint="Nothing can be typed, so the calendar is the only way in"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Période

selection à range fait de la valeur un début et une fin, le calendrier prenant le premier clic pour l'un et le second pour l'autre.

vue

Pick the first day, then the last. Typing falls back to read-only here.

2026-06-19 to 2026-06-26
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput, type DatePickerRange } from 'vectis-ui'

const period = ref<DatePickerRange>({ start: '2026-06-19', end: '2026-06-26' })
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="period"
      selection="range"
      label="Period"
      hint="Pick the first day, then the last. Typing falls back to read-only here."
      clearable
    />
    <output class="value" aria-label="Chosen period">
      {{ period.start ?? 'none' }} to {{ period.end ?? 'none' }}
    </output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.value {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Dates multiples

selection à multiple fait de la valeur une liste, un jour déjà présent en étant retiré par un nouveau clic.

vue

Click a day to add it, click it again to take it back

2026-06-05, 2026-06-12
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const dates = ref<string[]>(['2026-06-05', '2026-06-12'])
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="dates"
      selection="multiple"
      label="Dates"
      hint="Click a day to add it, click it again to take it back"
      clearable
    />
    <output class="value" aria-label="Chosen dates">{{ dates.join(', ') || 'none' }}</output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.value {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Raccourcis

Le slot #footer est une bande au pied du panneau, pour des actions ou pour les dates les plus demandées. Il reçoit close, de sorte qu'un bouton peut poser la valeur et fermer le panneau d'un coup.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDateInput } from 'vectis-ui'

const date = ref<string | null>(null)

/* The clock is read in the handler and never at setup: on the server there is no telling
   what day it is where the reader stands, and a value picked there would not survive
   hydration. */
function inDays(offset: number, close: () => void) {
  const day = new Date()
  day.setDate(day.getDate() + offset)
  const month = String(day.getMonth() + 1).padStart(2, '0')
  date.value = `${day.getFullYear()}-${month}-${String(day.getDate()).padStart(2, '0')}`
  close()
}
</script>

<template>
  <div class="column">
    <VDateInput v-model="date" mode="picker" label="Due date" clearable>
      <template #footer="{ close }">
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(0, close)">Today</VButton>
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(1, close)">
          Tomorrow
        </VButton>
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(7, close)">
          In a week
        </VButton>
      </template>
    </VDateInput>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Bornes et jours fermés

min et max bornent à la fois le choix et la navigation. disabledDates ferme des jours isolés, sous forme de liste ou de fonction répondant pour une date à la fois.

vue

Between 5 and 24 June 2026: the arrows stop at the bounds

Weekends are struck through: still reachable, never choosable

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const booking = ref<string | null>('2026-06-15')
const appointment = ref<string | null>('2026-06-16')

/* A list or a predicate: this one closes every weekend without naming a single date. */
function isWeekend(iso: string) {
  const day = new Date(`${iso}T00:00:00`).getDay()
  return day === 0 || day === 6
}
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="booking"
      min="2026-06-05"
      max="2026-06-24"
      label="Booking"
      hint="Between 5 and 24 June 2026: the arrows stop at the bounds"
      show-picker
    />

    <VDateInput
      v-model="appointment"
      :disabled-dates="isWeekend"
      label="Appointment"
      hint="Weekends are struck through: still reachable, never choosable"
      show-picker
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Pastilles

events dessine jusqu'à trois points sous un jour. Chacun accepte n'importe quelle couleur CSS et un label, qui est ce que lisent les technologies d'assistance.

vue

Open the calendar: June has four marked days

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput, type DatePickerEvent } from 'vectis-ui'

const date = ref<string | null>('2026-06-10')

/* Up to three dots a day. The colour is any CSS colour, so a token keeps it in step with
   the theme; without one the dot takes the accent. */
const events: DatePickerEvent[] = [
  { date: '2026-06-10', label: 'Kick-off' },
  { date: '2026-06-18', color: 'var(--vectis-color-danger)', label: 'Deadline' },
  { date: '2026-06-18', color: 'var(--vectis-color-warning)', label: 'Review' },
  { date: '2026-06-24', color: 'var(--vectis-color-success)', label: 'Release' },
]
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="date"
      :events="events"
      mode="picker"
      label="Sprint date"
      hint="Open the calendar: June has four marked days"
    />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Cellules de jour personnalisées

Le slot #day remplace le nombre à l'intérieur d'un jour et reçoit la date ISO ainsi que ce que la cellule sait d'elle-même : si elle appartient au mois affiché, si elle peut être choisie, si elle est sélectionnée, aujourd'hui, ou dans une période en cours de tracé.

vue

The slot replaces the day number, so what it draws follows the selection

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const night = ref<string | null>('2026-06-15')

/* Derived from the date itself and never drawn at random: the server and the browser have
   to render the same figure, or hydration finds two calendars. */
function priceFor(iso: string) {
  return 80 + ((Number(iso.slice(-2)) * 7) % 60)
}
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="night"
      mode="picker"
      min="2026-06-05"
      max="2026-06-24"
      label="Night"
      hint="The slot replaces the day number, so what it draws follows the selection"
    >
      <template #day="{ day, iso, inMonth, disabled }">
        <span class="number">{{ day }}</span>
        <small v-if="inMonth && !disabled" class="price">€{{ priceFor(iso) }}</small>
      </template>
    </VDateInput>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
.number {
  line-height: 1;
}
/* The price is set back from the number rather than given a colour of its own: mixing
   `currentcolor` towards transparent keeps it legible on the page AND on the accent a
   selected day is painted with, where a muted token would disappear. */
.price {
  font-size: var(--vectis-text-caption-size);
  line-height: 1;
  color: color-mix(in oklab, currentcolor 65%, transparent);
}
</style>

Effacement

clearable ajoute une croix qui vide la valeur, à gauche de l'icône de calendrier et non à sa place.

vue

The cross sits to the left of the calendar icon, never in its place

Emptying the field is then the reader's own business

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const withCross = ref<string | null>('2026-06-10')
const withoutCross = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="withCross"
      clearable
      show-picker
      label="Clearable"
      hint="The cross sits to the left of the calendar icon, never in its place"
    />

    <VDateInput
      v-model="withoutCross"
      show-picker
      label="Not clearable, the default"
      hint="Emptying the field is then the reader's own business"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Jours adjacents

showAdjacentDays remplit les coins de la grille avec les mois voisins, grisés et inertes. selectAdjacentDays rend ces jours choisissables, et en choisir un déplace le calendrier vers son mois.

vue

The grid starts and ends on the month itself

The corners are filled with the neighbouring months, greyed and inert

Choosing one moves the calendar to its month

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const hidden = ref<string | null>('2026-06-10')
const shown = ref<string | null>('2026-06-10')
const selectable = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="hidden"
      mode="picker"
      label="Hidden, the default"
      hint="The grid starts and ends on the month itself"
    />

    <VDateInput
      v-model="shown"
      show-adjacent-days
      mode="picker"
      label="Shown"
      hint="The corners are filled with the neighbouring months, greyed and inert"
    />

    <VDateInput
      v-model="selectable"
      select-adjacent-days
      mode="picker"
      label="Shown and choosable"
      hint="Choosing one moves the calendar to its month"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

États

invalid marque le champ en erreur. disabled le grise et empêche l'ouverture du panneau. readonly montre la valeur figée : rien ne se saisit et aucun calendrier n'est rendu, mais le champ garde son contraste et prend le focus.

vue

For a rule the browser cannot check by itself

Greyed through the colour tokens, and the panel can no longer be opened

No typing, no calendar, no clear cross

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const invalid = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="invalid"
      invalid
      show-picker
      label="Invalid"
      hint="For a rule the browser cannot check by itself"
    />

    <VDateInput
      model-value="2026-06-10"
      disabled
      show-picker
      label="Disabled, with a value"
      hint="Greyed through the colour tokens, and the panel can no longer be opened"
    />

    <VDateInput model-value="2026-06-10" disabled mode="picker" label="Disabled, picker only" />

    <!-- Frozen rather than out of reach: it still takes the focus and can be copied from. -->
    <VDateInput
      model-value="2026-06-10"
      readonly
      show-picker
      clearable
      label="Read-only"
      hint="No typing, no calendar, no clear cross"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Localisation

locale décide de l'ordre de saisie, du séparateur, des noms de mois et de jours et du premier jour de la semaine, et l'emporte sur la locale globale. displayFormat est un jeu d'options Intl pour écrire la date, et s'applique partout où rien n'est saisi.

vue

displayFormat is an Intl option bag, and it only applies where nothing is typed

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

/* The mask, the month names and the first day of the week all come from the tag. Each
   field keeps its own value so switching one does not disturb the others. */
const locales = ref([
  { tag: 'en-US', label: 'en-US, month first, weeks from Sunday', date: '2026-06-10' },
  { tag: 'en-GB', label: 'en-GB, day first, weeks from Monday', date: '2026-06-10' },
  { tag: 'de-DE', label: 'de-DE, dots for separators', date: '2026-06-10' },
  { tag: 'ja-JP', label: 'ja-JP, year first', date: '2026-06-10' },
])

const written = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-for="locale in locales"
      :key="locale.tag"
      v-model="locale.date"
      :locale="locale.tag"
      :label="locale.label"
      show-picker
    />

    <VDateInput
      v-model="written"
      locale="fr-FR"
      mode="picker"
      :display-format="{ dateStyle: 'full' }"
      label="fr-FR, written out in full"
      hint="displayFormat is an Intl option bag, and it only applies where nothing is typed"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Placement

placement nomme la direction d'ouverture préférée du panneau, au-dessus ou en dessous du champ.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const placements = ['bottom-start', 'bottom-end', 'top-start', 'top-end'] as const

const dates = ref<Record<string, string | null>>({
  'bottom-start': '2026-06-10',
  'bottom-end': '2026-06-10',
  'top-start': '2026-06-10',
  'top-end': '2026-06-10',
})
</script>

<template>
  <div class="grid">
    <VDateInput
      v-for="placement in placements"
      :key="placement"
      v-model="dates[placement]"
      :placement="placement"
      :label="placement"
      mode="picker"
    />
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDéfaut
selectionDatePickerSelection'single' | 'range' | 'multiple''single'
Ce qui est choisi : une date, une période entre deux, ou plusieurs dates séparées.
localestringaucune
Une locale BCP 47, qui décide des noms de mois et de jours, du premier jour de la semaine et de l'ordre dans lequel le champ se saisit. Elle l'emporte sur la locale globale du design system et retombe dessus.
firstDayOfWeeknumberaucune
Force le jour où commencent les semaines, de 0 pour dimanche à 6 pour samedi.
minstringaucune
La première date qui peut être choisie, en chaîne ISO.
maxstringaucune
La dernière date qui peut être choisie, en chaîne ISO.
disabledDatesDatePickerMatcheraucune
Les dates qui ne peuvent pas être choisies, en liste ou en fonction.
showAdjacentDaysbooleanfalse
Remplit les coins de la grille avec les jours grisés des mois voisins.
selectAdjacentDaysbooleanfalse
Permet de cliquer ces jours voisins, ce qui implique de les afficher.
eventsDatePickerEvent[]aucune
Les événements à marquer sous les jours concernés.
modeDateInputMode'picker' | 'input''input'
Si le champ peut être SAISI, dans la forme numérique de la langue du lecteur, ou s'il se remplit depuis le seul calendrier, ce qui est picker. La saisie est réservée au choix d'une date UNIQUE : une période ou une liste retombe sur picker, faute de façon sensée de saisir l'une ou l'autre. C'est une autre question que readonly, qui gèle le champ par toutes les voies à la fois.
showPickerbooleanfalse
Propose le sélecteur de date à côté d'un champ saisissable : une icône en fin de champ, et un panneau qui s'ouvre au focus. Cela ne signifie rien en mode picker, où le calendrier est déjà la seule façon de choisir.
labelstringaucune
Le libellé au-dessus du champ.
hintstringaucune
Une ligne d'aide sous le champ.
placeholderstringaucune
Ce que dit le champ quand il est vide.
sizeDateInputSize'sm' | 'md' | 'lg''md'
La hauteur du champ : 32, 40 ou 48 pixels.
compactbooleanfalse
Retire 4px à la hauteur.
disabledbooleanfalse
Rend le champ inutilisable, grisé par les tokens de couleur.
readonlybooleanfalse
Montre la date sans permettre de la changer : rien ne se tape, il n'y a ni calendrier ni croix de vidage, et les attributs qui annonçaient un panneau disparaissent avec eux. Le champ garde le focus et reste copiable, ce qui le distingue de disabled.
invalidbooleanfalse
Marque le champ comme invalide, pour une règle à vous.
iconStartIconSourceaucune
Une icône dans le champ, au début. Décorative jusqu'à ce qu'un écouteur @click:icon-start en fasse un bouton.
iconStartLabelstringaucune
Ce que fait l'icône de début, en mots, une fois cliquable.
pickerIconLabelstringaucune
Ce que fait l'icône de fin, en mots. Elle nomme le bouton qui ouvre le calendrier, et sa valeur par défaut vient du dictionnaire du design system.
loadingbooleanfalse
Affiche une roue à la place de l'icône du calendrier. Elle dit que quelque chose se charge et ne change rien d'autre : le champ reste saisissable et le panneau s'ouvre toujours.
loadingTextstringaucune
Ce que les lecteurs d'écran annoncent pendant que la roue tourne. Sa valeur par défaut vient du dictionnaire du design system.
clearablebooleanfalse
Propose une croix qui vide la valeur, affichée avant l'icône de fin.
clearLabelstringaucune
Ce que fait cette croix, en mots. Sa valeur par défaut vient du dictionnaire du design system.
pickerIconIconSourcecalendar_today
L'icône qui ouvre le sélecteur de date, en fin de champ. La croix d'effacement apparaît à sa gauche plutôt qu'à sa place, et aucune icône n'est rendue du tout quand il n'y a pas de panneau à ouvrir.
displayFormatIntl.DateTimeFormatOptions{ day: 'numeric', month: 'short', year: 'numeric' }
Comment la date est ÉCRITE dans le champ. Sans effet sur un champ en cours de saisie, qui montre nécessairement la forme numérique que l'on tape : cette prop concerne donc le mode picker, ainsi que les sélections de période et de liste.
placementDateInputPlacement'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end''bottom-start'
Où le panneau s'ouvre par rapport au champ.
v-modelDatePickerValuenull
La ou les dates choisies, dans la forme que selection réclame. Pendant la saisie, la valeur n'est écrite qu'une fois que ce qui a été entré est une date complète et acceptable ; une entrée inachevée ou refusée la laisse intacte et est annulée quand le lecteur quitte le champ.

Événements

ÉvénementType
click:icon-start[event: MouseEvent]
L'icône de début a été cliquée. Attacher cet écouteur est ce qui en fait un vrai bouton, qui demande alors iconStartLabel.
clear[]
La croix de vidage a vidé le champ. La valeur est déjà remise à zéro.

Slots

SlotType
start{}
Du contenu au début du champ, rendu après iconStart plutôt qu'à sa place.
value-end{}
Des contrôles à vous à l'intérieur du champ, placés avant ceux que le champ possède : la croix d'effacement et l'icône qui ouvre le panneau. Ces deux-là sont l'affordance propre du composant, ce qui explique l'absence de slot end ici.
dayDatePickerDaySlotProps
Ce que montre une cellule de jour, transmis tel quel au calendrier.
footerDateInputFooterSlotProps
La bande au pied du panneau : des actions, ou des dates prédéfinies comme aujourd'hui. Elle reçoit close, ce qui permet à l'un de ces boutons de refermer le panneau.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export interface DateInputFooterSlotProps {
  close: () => void
}
export interface DatePickerDaySlotProps {
  iso: string
  day: number
  inMonth: boolean
  disabled: boolean
  selected: boolean
  today: boolean
  inRange: boolean
  events: DatePickerEvent[]
}
export interface DatePickerEvent {
  date: string
  color?: string
  label?: string
}
export type DatePickerMatcher = string[] | ((iso: string) => boolean)
export interface DatePickerRange {
  start: string | null
  end: string | null
}
export type DatePickerValue = string | null | DatePickerRange | string[]
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender