Kosmesis
Components

Steps

A numbered step indicator with a connecting rail between steps. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add steps
pnpm dlx kosmesis add steps
yarn dlx kosmesis add steps
bunx kosmesis add steps

Install the following dependencies:

npm install @morphos/icons
pnpm add @morphos/icons
yarn add @morphos/icons
bun add @morphos/icons

Copy and paste the following code into your project.

steps.tsx
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/icons
pnpm add @praxisjs/css @morphos/icons
yarn add @praxisjs/css @morphos/icons
bun add @praxisjs/css @morphos/icons

Copy and paste the following code into your project.

steps.tsx
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

PropTypeDefault
orientation"horizontal" | "vertical""horizontal"
classstring

Step

PropTypeDefault
stepnumber
status"complete" | "current" | "upcoming""upcoming"
titleChildren
descriptionChildren
classstring

On this page