Timeline
A list of things that happened, in order, joined by a line.
Install
If this is the first component in the project, set up once. This writes ermine.json and copies the theme stylesheet. The installation guide covers Vue, Nuxt and Astro.
npx ermineui initpnpm dlx ermineui inityarn dlx ermineui initbunx ermineui initThen add the component:
npx ermineui add timelinepnpm dlx ermineui add timelineyarn dlx ermineui add timelinebunx ermineui add timelineThat copies 3 files into src/components/ui/timeline/. Files you already have are kept.
Use
import { Timeline, TimelineItem } from '@/components/ui/timeline'<Tour v-model:open="touring" :steps="[
{ target: '#deploy-button', title: 'Deploy from here', description: 'Or just push to main.' },
{ target: '#search', title: 'Find anything', side: 'bottom' },
]" />
<Banner v-model:open="showBanner" variant="info" dismissible>
Scheduled maintenance on Sunday.
<template #action><a href="/status">Details</a></template>
</Banner>
<Timeline>
<TimelineItem title="Promoted to production" time="12 min ago" tone="success">4f2c9a1 by Ines.</TimelineItem>
</Timeline>
<CodeBlock :code="snippet" filename="nuxt.config.ts" line-numbers />
<CopyButton value="dpl_12d5" label="Copy deployment id" />
Press <Kbd>⌘</Kbd> <Kbd>K</Kbd>In Nuxt, with the auto-import setting, the tags work in templates without the import. In Astro, use it inside a Vue island.
What gets copied
| File | Where it goes |
|---|---|
Timeline.vue | src/components/ui/timeline/Timeline.vue |
TimelineItem.vue | src/components/ui/timeline/TimelineItem.vue |
index.ts | src/components/ui/timeline/index.ts |
The folders are the suggested ones. ermine.json says where yours are.
Source
This is exactly what the command copies. To install by hand, create each file below and the helpers listed above.
Timeline.vue 106 lines
<template>
<ol class="er-timeline"><slot /></ol>
</template>
<style>
@layer components {
.er-timeline {
display: grid;
margin: 0;
padding: 0;
font-size: 0.875rem;
list-style: none;
}
.er-timeline-item {
position: relative;
display: grid;
grid-template-columns: 1.5rem 1fr;
gap: 0 0.75rem;
padding-bottom: 1.25rem;
}
.er-timeline-item:last-child {
padding-bottom: 0;
}
/* The line runs from under this marker to the next item's marker. */
.er-timeline-item:not(:last-child)::before {
content: '';
position: absolute;
top: 1.625rem;
bottom: 0.125rem;
left: calc(0.75rem - 0.5px);
width: 1px;
background: var(--er-border);
}
.er-timeline-marker {
--er-timeline-color: var(--er-muted-foreground);
display: grid;
place-items: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: color-mix(in oklab, var(--er-timeline-color) 14%, var(--er-background));
color: var(--er-timeline-color);
}
.er-timeline-item[data-tone='success'] .er-timeline-marker {
--er-timeline-color: var(--er-success);
}
.er-timeline-item[data-tone='warning'] .er-timeline-marker {
--er-timeline-color: var(--er-warning);
}
.er-timeline-item[data-tone='destructive'] .er-timeline-marker {
--er-timeline-color: var(--er-destructive);
}
.er-timeline-marker svg {
width: 0.75rem;
height: 0.75rem;
}
/* Without an icon the marker is a plain dot. */
.er-timeline-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: currentColor;
}
.er-timeline-body {
display: grid;
gap: 0.125rem;
min-width: 0;
padding-top: 0.125rem;
}
.er-timeline-heading {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 0 0.75rem;
}
.er-timeline-title {
font-weight: var(--er-weight-medium);
}
.er-timeline-time {
color: var(--er-muted-foreground);
font-size: 0.8125rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.er-timeline-content {
color: var(--er-muted-foreground);
line-height: 1.5;
}
}
</style>TimelineItem.vue 30 lines
<script setup lang="ts">
withDefaults(
defineProps<{
title: string
/** When it happened, already written out the way it should read ("2 hours ago", "Oct 6, 14:20"). */
time?: string
/** For machines: the same moment as an ISO date, e.g. "2026-10-06T14:20". */
datetime?: string
/** Colours the marker. Say what happened in the title too — colour alone is not enough. */
tone?: 'default' | 'success' | 'warning' | 'destructive'
}>(),
{ tone: 'default' },
)
</script>
<template>
<li class="er-timeline-item" :data-tone="tone">
<span class="er-timeline-marker" aria-hidden="true">
<slot name="icon"><span class="er-timeline-dot" /></slot>
</span>
<div class="er-timeline-body">
<div class="er-timeline-heading">
<span class="er-timeline-title">{{ title }}</span>
<time v-if="time" class="er-timeline-time" :datetime="datetime">{{ time }}</time>
</div>
<div v-if="$slots.default" class="er-timeline-content"><slot /></div>
</div>
</li>
</template>index.ts 4 lines
/** Timeline — a list of things that happened, in order, joined by a line. */
export { default as Timeline } from './Timeline.vue'
export { default as TimelineItem } from './TimelineItem.vue'