Components
Steps
A numbered step indicator with a connecting rail between steps. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add stepspnpm dlx kosmesis add stepsyarn dlx kosmesis add stepsbunx kosmesis add stepsInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";export type StepStatus = "complete" | "current" | "upcoming";export interface StepsProps { orientation?: "horizontal" | "vertical"; class?: string; id?: string; children?: Children;}@Component()export class Steps extends StatelessComponent<StepsProps> { render() { const { orientation = "horizontal", class: cls, id, children } = this.props; return ( <ol id={id} data-slot="steps" data-orientation={orientation} class={cn("flex w-full items-start gap-2 data-[orientation=vertical]:flex-col data-[orientation=vertical]:gap-6", cls)} > {children} </ol> ); }}export interface StepProps { step: number; status?: StepStatus; title: Children; description?: Children; class?: string;}/** The connecting line is a single absolutely-positioned span reading ancestor `in-data-*` variants — no separate connector child needed. */@Component()export class Step extends StatelessComponent<StepProps> { render() { const { step, status = "upcoming", title, description, class: cls } = this.props; return ( <li data-slot="step" data-status={status} class={cn( "group relative flex flex-1 flex-col items-center gap-2 text-center", "in-data-[orientation=vertical]:flex-row in-data-[orientation=vertical]:items-start in-data-[orientation=vertical]:text-left", cls, )} > <span aria-hidden class={cn( "absolute top-4 left-[calc(50%+1.25rem)] right-[calc(-50%+0.75rem)] h-0.5 -translate-y-1/2 bg-border group-last:hidden", "in-data-[status=complete]:bg-primary", "in-data-[orientation=vertical]:top-[calc(1rem+1.25rem)] in-data-[orientation=vertical]:right-auto in-data-[orientation=vertical]:bottom-[calc(-100%+1.25rem)] in-data-[orientation=vertical]:left-4 in-data-[orientation=vertical]:h-auto in-data-[orientation=vertical]:w-0.5 in-data-[orientation=vertical]:translate-y-0", )} /> <span class={cn( "relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 border-muted bg-background text-sm font-medium text-muted-foreground transition-colors", "in-data-[status=current]:border-primary in-data-[status=current]:text-primary", "in-data-[status=complete]:border-primary in-data-[status=complete]:bg-primary in-data-[status=complete]:text-primary-foreground", )} > {status === "complete" ? <Icon name="Check" size={16} /> : step} </span> <div class="flex flex-col gap-0.5 px-1 in-data-[orientation=vertical]:px-0 in-data-[orientation=vertical]:pb-8"> <span class="text-sm font-medium text-foreground in-data-[status=upcoming]:text-muted-foreground">{title}</span> {description ? <span class="text-xs text-muted-foreground">{description}</span> : null} </div> </li> ); }}Install the following dependencies:
npm install @praxisjs/css @morphos/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class StepsStyles extends Stylesheet { $root = this.css({ display: "flex", width: "100%", alignItems: "flex-start", gap: "0.5rem" }).on( '&[data-orientation="vertical"]', { flexDirection: "column", gap: "1.5rem" }, ); $item = this.css({ position: "relative", display: "flex", flex: "1 1 0%", flexDirection: "column", alignItems: "center", gap: "0.5rem", textAlign: "center", }).on('[data-orientation="vertical"] &', { flexDirection: "row", alignItems: "flex-start", textAlign: "left" }); $connector = this.css({ position: "absolute", top: "1rem", left: "calc(50% + 1.25rem)", right: "calc(-50% + 0.75rem)", height: "2px", transform: "translateY(-50%)", backgroundColor: t.border, }) .on('[data-status="complete"] &', { backgroundColor: t.primary }) .on('[data-slot="step"]:last-child &', { display: "none" }) .on('[data-orientation="vertical"] &', { top: "calc(1rem + 1.25rem)", right: "auto", bottom: "calc(-100% + 1.25rem)", left: "1rem", height: "auto", width: "2px", transform: "none", }); $indicator = this.css({ position: "relative", zIndex: 10, display: "flex", height: "2rem", width: "2rem", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "9999px", border: `2px solid ${t.muted}`, backgroundColor: t.background, fontSize: "0.875rem", fontWeight: 500, color: t.mutedForeground, transition: "color 150ms ease, border-color 150ms ease, background-color 150ms ease", }) .on('[data-status="current"] &', { borderColor: t.primary, color: t.primary }) .on('[data-status="complete"] &', { borderColor: t.primary, backgroundColor: t.primary, color: t.primaryForeground }); $content = this.css({ display: "flex", flexDirection: "column", gap: "0.125rem", padding: "0 0.25rem" }).on( '[data-orientation="vertical"] &', { padding: "0 0 2rem 0" }, ); $title = this.css({ fontSize: "0.875rem", fontWeight: 500, color: t.foreground }).on('[data-status="upcoming"] &', { color: t.mutedForeground, }); $description = this.css({ fontSize: "0.75rem", color: t.mutedForeground });}export type StepStatus = "complete" | "current" | "upcoming";export interface StepsProps { orientation?: "horizontal" | "vertical"; class?: string; id?: string; children?: Children;}@Component()export class Steps extends StatelessComponent<StepsProps> { @Styled(StepsStyles) $s!: StepsStyles; render() { const { orientation = "horizontal", class: cls, id, children } = this.props; return ( <ol id={id} data-slot="steps" data-orientation={orientation} class={cx(this.$s.$root, cls)}> {children} </ol> ); }}export interface StepProps { step: number; status?: StepStatus; title: Children; description?: Children; class?: string;}/** The connecting line is a single absolutely-positioned span reading ancestor selectors for status/orientation — no separate connector child needed. */@Component()export class Step extends StatelessComponent<StepProps> { @Styled(StepsStyles) $s!: StepsStyles; render() { const { step, status = "upcoming", title, description, class: cls } = this.props; return ( <li data-slot="step" data-status={status} class={cx(this.$s.$item, cls)}> <span aria-hidden class={this.$s.$connector} /> <span class={this.$s.$indicator}>{status === "complete" ? <Icon name="Check" size={16} /> : step}</span> <div class={this.$s.$content}> <span class={this.$s.$title}>{title}</span> {description ? <span class={this.$s.$description}>{description}</span> : null} </div> </li> ); }}Examples
About
Purely presentational — no Morphos equivalent. Each Step carries its own status
("complete" | "current" | "upcoming"); the connecting rail to the next step is a single
absolutely-positioned span that reads its own ancestor's data-status/data-orientation
attributes, so no separate connector element needs to be rendered between steps.
Usage
import { Step, Steps } from "@/components/ui/steps";
<Steps>
<Step step={1} status="complete" title="Account" description="Create your account" />
<Step step={2} status="current" title="Profile" description="Tell us about you" />
<Step step={3} status="upcoming" title="Confirm" description="Review and submit" />
</Steps>Props
Steps
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
class | string | — |
Step
| Prop | Type | Default |
|---|---|---|
step | number | — |
status | "complete" | "current" | "upcoming" | "upcoming" |
title | Children | — |
description | Children | — |
class | string | — |