A text element that carries one of the type roles. Each role is a complete recipe of tokens, so a heading, a label and a caption are named rather than described by hand.
Usage
vue
A design system is a contract, not a folder of components.
<script setup lang="ts">
import { VTypography } from 'vectis-ui'
</script>
<template>
<VTypography>A design system is a contract, not a folder of components.</VTypography>
</template>
Examples
Variants
variant offers fourteen roles, each one a complete recipe rather than a size: the weight, the line height and, where the role calls for it, the letter spacing and a monospaced family come with it.
vue
display
Pack my box with five dozen jugs.
heading-1
Pack my box with five dozen jugs.
heading-2
Pack my box with five dozen jugs.
heading-3
Pack my box with five dozen jugs.
heading-4
Pack my box with five dozen jugs.
subtitle
Pack my box with five dozen jugs.
body-xl
Pack my box with five dozen jugs.
body-lg
Pack my box with five dozen jugs.
body-md
Pack my box with five dozen jugs.
body-sm
Pack my box with five dozen jugs.
label
Pack my box with five dozen jugs.
caption
Pack my box with five dozen jugs.
overline
Pack my box with five dozen jugs.
code
Pack my box with five dozen jugs.
<script setup lang="ts">
import { VTypography, type TypographyVariant } from 'vectis-ui'
const variants: TypographyVariant[] = [
'display',
'heading-1',
'heading-2',
'heading-3',
'heading-4',
'subtitle',
'body-xl',
'body-lg',
'body-md',
'body-sm',
'label',
'caption',
'overline',
'code',
]
</script>
<template>
<!-- Each role is a complete recipe: size, weight, line height, and where the role
calls for it the letter spacing and a monospaced family. Nothing here is set one
property at a time. -->
<div class="table">
<template v-for="variant in variants" :key="variant">
<VTypography variant="code" as="span" tone="muted">{{ variant }}</VTypography>
<VTypography :variant="variant" as="p">Pack my box with five dozen jugs.</VTypography>
</template>
</div>
</template>
<style scoped>
.table {
display: grid;
grid-template-columns: max-content 1fr;
align-items: baseline;
gap: var(--vectis-space-3) var(--vectis-space-5);
}
</style>
Tones
tone is the colour of the text, said as a meaning. default sets no colour at all, so the text inherits whatever surrounds it, and on-inverse names its ground instead, for text the inherited colour would not survive.
vue
default: pack my box with five dozen jugs.
muted: pack my box with five dozen jugs.
subtle: pack my box with five dozen jugs.
accent: pack my box with five dozen jugs.
danger: pack my box with five dozen jugs.
success: pack my box with five dozen jugs.
warning: pack my box with five dozen jugs.
on-inverse: pack my box with five dozen jugs.
<script setup lang="ts">
import { VTypography, type TypographyTone } from 'vectis-ui'
// `on-inverse` is left out of the loop: it only means anything against a dark ground,
// so it is shown on one below.
const tones: TypographyTone[] = [
'default',
'muted',
'subtle',
'accent',
'danger',
'success',
'warning',
]
</script>
<template>
<div class="demo">
<!-- `default` sets no colour at all, so the text takes whatever surrounds it. That
is what lets the same component sit inside a coloured toast or on an inverted
surface without being told which one it is on. -->
<VTypography v-for="tone in tones" :key="tone" :tone="tone">
{{ tone }}: pack my box with five dozen jugs.
</VTypography>
<!-- The one tone that names its ground rather than its meaning, for text the
inherited colour would not survive. -->
<div class="inverse">
<VTypography tone="on-inverse">on-inverse: pack my box with five dozen jugs.</VTypography>
</div>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-2);
}
.inverse {
margin-block-start: var(--vectis-space-2);
padding: var(--vectis-space-2) var(--vectis-space-3);
border-radius: var(--vectis-radius-surface);
background: var(--vectis-color-surface-inverse);
}
</style>
The tag it renders
Each variant already renders a sensible tag, h1 to h4 for the headings, code for code and p or span for the rest. as is for the cases where the meaning and the look part ways, and everything else falls through to that tag.
vue
Rendered as an h4, which is its default
Reads as an h4, is really an h2
<script setup lang="ts">
import { VInput, VTypography } from 'vectis-ui'
</script>
<template>
<div class="demo">
<!-- Each variant already renders a sensible tag: h1 to h4 for the headings, p for
the body roles, span for the small ones, code for code. -->
<VTypography variant="heading-4">Rendered as an h4, which is its default</VTypography>
<!-- `as` is for the cases where the meaning and the look part ways: a section that
is an h2 in the document but should read at the size of an h4. -->
<VTypography variant="heading-4" as="h2">Reads as an h4, is really an h2</VTypography>
<!-- With a single root and no attributes of its own, everything falls through. A
label variant rendered as a real label, pointing at a real field, is a working
pair rather than a resemblance. -->
<VTypography variant="label" as="label" for="reference">Reference</VTypography>
<VInput id="reference" model-value="INV-2481" />
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
max-inline-size: 22rem;
}
</style>
Truncating to one line
truncate keeps the text to one line, ended with an ellipsis. The element needs a width to be cut against, and with nothing to overflow the text simply stays whole.
vue
A title far too long to sit on one line of a narrow column without being cut
A title far too long to sit on one line of a narrow column without being cut
<script setup lang="ts">
import { VTypography } from 'vectis-ui'
const LONG = 'A title far too long to sit on one line of a narrow column without being cut'
</script>
<template>
<div class="demo">
<!-- One line, ended with an ellipsis. The element needs a width to be cut against:
as a block or a flex item it takes its parent's, which is what the box below
gives it. -->
<div class="box">
<VTypography truncate>{{ LONG }}</VTypography>
</div>
<!-- With nothing to overflow, there is nothing to cut and the text simply stays
whole: the same element, left to size itself. -->
<VTypography truncate>{{ LONG }}</VTypography>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: var(--vectis-space-4);
}
.box {
inline-size: 16rem;
padding: var(--vectis-space-3);
border: 1px dashed var(--vectis-color-border);
border-radius: var(--vectis-radius-surface);
}
</style>
A block of text
The component carries no margin of its own: the space between two pieces of text belongs to the layout holding them, usually a grid or a flex column with a gap.
vue
Release notes
What changed in this version
A short standfirst, set one step above the body so the eye knows where to start.
The body role carries the line height a paragraph needs at its size, and long words break rather than push a column out of shape. An inline role sits inside it without disturbing the line: a value such as --vectis-text-body-md-size keeps its own family and size while the leading stays the paragraph's.
Published on 12 September 2026
<script setup lang="ts">
import { VTypography } from 'vectis-ui'
</script>
<template>
<!-- Several roles composed into one block. The component carries no margin of its
own, deliberately: the space between two pieces of text belongs to the layout
holding them, so stacking these without a gap here would leave them touching. -->
<article class="article">
<VTypography variant="overline" tone="muted">Release notes</VTypography>
<VTypography variant="heading-3" as="h2">What changed in this version</VTypography>
<VTypography variant="body-lg" tone="muted">
A short standfirst, set one step above the body so the eye knows where to start.
</VTypography>
<VTypography variant="body-md">
The body role carries the line height a paragraph needs at its size, and long words break
rather than push a column out of shape. An inline role sits inside it without disturbing the
line: a value such as
<VTypography variant="code" as="span">--vectis-text-body-md-size</VTypography>
keeps its own family and size while the leading stays the paragraph's.
</VTypography>
<VTypography variant="caption" tone="subtle">Published on 12 September 2026</VTypography>
</article>
</template>
<style scoped>
.article {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
max-inline-size: 34rem;
}
</style>
The role the text plays, which selects a complete recipe of typographic tokens: size, weight, line height and, where the role calls for it, letter spacing and a monospaced family.
as
string
none
The HTML tag to render. Each variant already has a sensible default (h1 to h4, p, span, code), so this is for the cases where the meaning and the look differ: a subtitle that is really an h2, or a label attached to a field.
The colour of the text. default sets none at all, so the text inherits from whatever surrounds it, which is what lets the same component sit on an inverted surface or inside a coloured toast.
truncate
boolean
false
Cuts the text to one line and ends it with an ellipsis. The element needs a width to be cut against, as a block or a flex item; otherwise there is nothing to overflow and the text stays whole.