██╗ ██╗████████╗ ██╗ ██████╗ ██████╗
██║ ██║╚══██╔══╝███║██╔ ██║██╔╔══██╗
██║ ██║ ██║ ╚██║██║ ██║██║ ██║
╚██╗ ██╔╝ ██║ ██║██║ ██║██║ ██║
╚████╔╝ ██║ ██║╚██████╔╝╚██████╔╝
╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
A retro-terminal, motion-first Vue 3 component library.
Phosphor green. Hard cuts. Zero border-radius. No springs.
vt100 is a component library for Vue 3 that treats the constraints of an old CRT terminal as
an actual design system, not a texture applied on top of a normal UI kit. One accent color at a
time. Flat rectangles, no rounded corners anywhere. Motion that jumps in hard steps() cuts
instead of easing. State communicated by glyph and shape, never by a second hue.
It's not a theme — it's ~98 components (forms, overlays, data display, navigation, feedback, charts) that all follow the same small set of rules, so the 90th component you drop into a page looks like it belongs next to the 1st.
$ npm install vt100-ui// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import 'vt100-ui/style.css'
createApp(App).mount('#app')<script setup lang="ts">
import { VtButton, VtInput, VtField } from 'vt100-ui'
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<VtField label="name" hint="what should we call you?">
<template #default="{ id }">
<VtInput :id="id" v-model="name" placeholder="joris" />
</template>
</VtField>
<VtButton variant="primary">continue</VtButton>
</template>
Four built-in accent themes — green (default), amber, white, blue — swapped at runtime via a
data-theme attribute on :root. Every color-bearing token in the library derives from two CSS
custom properties, so a fifth theme is just two lines:
[data-theme='sunset'] {
--vt-accent: #ff5f1f;
--vt-accent-rgb: 255, 95, 31;
}Or reach for the composable and skip the CSS entirely:
import { useTheme } from 'vt100-ui'
const { theme, setTheme, toggleTheme, setCustomAccent } = useTheme()
setTheme('amber')
setCustomAccent('#ff5f1f') // any hex color, persisted across reloads<VtThemeToggle /> wraps this into a single drop-in control.
Isolated components in a gallery only tell you so much. Running the playground (npm run dev)
also gets you four full page mockups under the examples nav group — a SaaS landing page, an
admin dashboard, a blog/docs article, and a login screen — built entirely from real vt100
components wired together, not cropped screenshots.
Both the marketing landing page and the full docs/gallery app are also deployed to GitHub Pages: joris63.github.io/vt100 for the landing page, and joris63.github.io/vt100/docs for the docs app itself.
98 components across 7 categories. Every one ships its own hand-written .d.ts — import only
what you use.
forms — 24
Button · Input · Textarea · NumberInput · Slider · RangeSlider · Toggle · ToggleGroup · Checkbox · RadioGroup · Select · Combobox · DatePicker · Calendar · Rating · FileUpload · SegmentedControl · ColorPicker · PinInput · TagsInput · PasswordInput · SplitButton · CopyButton · Field
feedback — 10
Alert · Toast · ProgressBar · Spinner · Skeleton · EmptyState · LoadingBar · Banner · BootSequence · MatrixRain
overlays — 13
Dialog · Drawer · Popover · Menu · CommandPalette · ContextMenu · ConfirmDialog · Tooltip · HoverCard · Tour · Lightbox · NotificationCenter · Popconfirm
navigation — 11
Breadcrumb · Pagination · Tabs · Accordion · Collapsible · Stepper · TreeSelect · BackToTop · Wizard · NavMenu · AnchorNav
data display — 29
Badge · Tag · Card · DataTable · Avatar · AvatarGroup · Kbd · Divider · Timeline · Statistic · ScrollArea · Terminal · List · AspectRatio · Marquee · CodeBlock · Sparkline · DiffViewer · Clock · QrCode · JsonViewer · BarChart · LineChart · Gauge · Ticker · DescriptionList · VirtualList · Countdown · Heatmap
theming — 1
ThemeToggle
utilities — 9
VisuallyHidden · Stack · Container · Splitter · Masonry · CrtOverlay · StatusBar · Affix · InfiniteScroll
Every component in the library is checked against the same small set of constraints:
- One accent color at a time. State/tone is glyph or shape, never a second hue.
- Zero border-radius, everywhere. Enforced globally, no per-component overrides.
- Motion uses
steps()easing. Hard cuts, never a smooth or spring curve. - Every
:hoveris gated behind@media (hover: hover) and (pointer: fine)— no sticky hover states on touch. prefers-reduced-motion: reduceis respected everywhere motion appears, both at the token level and per-component.
| Command | What it does |
|---|---|
npm run dev |
Playground app, local dev server |
npm run build |
Type-check + build the playground (docs app) → dist-playground/ |
npm run build:lib |
Build the publishable package → dist/ |
npm run type-check |
vue-tsc --build |
npm run test:unit |
Vitest, watch mode |
npm run lint |
oxlint + eslint, both with --fix |
npm run format |
prettier --write src/ |
npx vitest run # run the full suite once
npx vitest run path/to/File.spec.ts # a single spec file
npx vitest run -t "test name substring" # a single test by nameEvery component lives at src/components/<Name>/<Name>.vue next to its own <Name>.spec.ts and
a <Name>Gallery.vue doc page under src/views/. Shared cross-cutting behavior lives in
src/composables/ — useTheme, useToast, useConfirm, useFloatingPanel, and friends —
rather than being reimplemented per component.
Published as vt100-ui (the plain vt100 name is
already taken by an unrelated package). .github/workflows/publish.yml handles it — publishing
a GitHub Release triggers a build + test + npm publish. To cut a release:
npm version patch # or minor / major — bumps package.json and tags the commit
git push --follow-tagsThen draft a GitHub Release from that tag (Releases → Draft a new release). The workflow needs an
NPM_TOKEN repo secret — an npm Automation token with publish rights on vt100-ui, added under
Settings → Secrets and variables → Actions.
The landing page and the docs app both deploy to GitHub Pages together on every push to master,
via .github/workflows/deploy-pages.yml — see > see it in context_ above for the live URLs.
monospace or nothing.