How It Works — Tabbed Steps
blockmarketingStep explainer where a vertical tab list of stages swaps a paired detail panel showing the action, the expected result, and a primitive-built screen for that step.
Installation
$pnpm dlx uipkge-ng@latest add how-it-works-tabbed-steps$npx uipkge-ng@latest add how-it-works-tabbed-steps$yarn dlx uipkge-ng@latest add how-it-works-tabbed-steps$bunx uipkge-ng@latest add how-it-works-tabbed-stepsManifest:
https://uipkge.dev/r/angular/how-it-works-tabbed-steps.jsonInstalls to:src/app/components/blocks/how-it-works-tabbed-steps/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TabbedStepRowinterface TabbedStepRow {
name: string
detail: string
}TabbedStepinterface TabbedStep {
id: string
label: string
action: string
result: string
rows: TabbedStepRow[]
}Files installed (2)
src/app/components/blocks/how-it-works-tabbed-steps/how-it-works-tabbed-steps.component.ts6.8 kB
// Step explainer where a vertical tab list of stages swaps a paired detail panel showing // the action, the expected result, and a primitive-built screen for that step. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core' import { Check, LucideAngularModule } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export interface TabbedStepRow { name: string detail: string } export interface TabbedStep { id: string label: string action: string result: string rows: TabbedStepRow[] } const steps: TabbedStep[] = [ { id: 'connect', label: 'Connect', action: 'Point us at the warehouse with a read-only role.', result: 'Schema mirrored, 412 tables catalogued, nothing copied out.', rows: [ { name: 'warehouse.orders', detail: '2.4M rows' }, { name: 'warehouse.refunds', detail: '88k rows' }, { name: 'warehouse.fx_rates', detail: 'daily' }, ], }, { id: 'define', label: 'Define', action: 'Author the metric as a file and open a pull request.', result: 'Reviewed by its owner, with a diff and a revert path.', rows: [ { name: 'revenue_net', detail: 'finance-analytics' }, { name: 'margin_by_channel', detail: 'finance-analytics' }, { name: 'pipeline_weighted', detail: 'revops' }, ], }, { id: 'certify', label: 'Certify', action: 'Reconcile the definition against the prior four quarters.', result: 'Sign-off recorded; the metric can now be published.', rows: [ { name: 'Q4 2025', detail: 'matched' }, { name: 'Q3 2025', detail: 'matched' }, { name: 'Q2 2025', detail: 'matched' }, ], }, { id: 'publish', label: 'Publish', action: 'Release to every consumer at once.', result: 'Dashboards, exports, and the API resolve the same number.', rows: [ { name: 'Exec summary', detail: 'live' }, { name: 'Finance close pack', detail: 'live' }, { name: 'Partner API', detail: 'live' }, ], }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-how-it-works-tabbed-steps, [ui-how-it-works-tabbed-steps]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiSeparatorComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <section data-slot="how-it-works-tabbed-steps" [class]="rootClass"> <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28"> <div class="max-w-2xl"> <span ui-badge variant="secondary">How it works</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Pick a stage to see what it produces</h2> </div> <ui-tabs [value]="activeStep()" (valueChange)="setActiveStep($event)" orientation="vertical" class="mt-10"> <div class="grid gap-8 lg:grid-cols-[16rem_1fr] lg:gap-12"> <!-- Vertical tab rail: orientation is set on both list and triggers so arrow keys move up and down rather than left and right. --> <ui-tabs-list variant="underline" orientation="vertical" class="h-auto w-full flex-col items-stretch"> @for (step of allSteps; track step.id; let index = $index) { <button ui-tabs-trigger [value]="step.id" variant="underline" orientation="vertical" class="justify-start gap-3" > <span class="text-muted-foreground font-mono text-xs">{{ pad(index + 1) }}</span> {{ step.label }} </button> } </ui-tabs-list> @for (step of allSteps; track step.id) { <ui-tabs-content [value]="step.id" class="mt-0"> <!-- min-h keeps the panel from collapsing between stages. --> <div class="grid min-h-[20rem] gap-6 sm:grid-cols-2"> <div> <p class="text-muted-foreground text-sm">You do</p> <p class="mt-1.5 text-lg leading-snug font-medium text-balance">{{ step.action }}</p> <ui-separator class="my-5" /> <p class="text-muted-foreground text-sm">You get</p> <p class="mt-1.5 flex items-start gap-2 text-sm leading-relaxed"> <lucide-icon [img]="Check" class="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" /> <span>{{ step.result }}</span> </p> <button ui-button variant="link" class="mt-5 h-auto p-0">Read this step in the docs</button> </div> <div ui-card class="self-start"> <div ui-card-content class="p-0"> <div class="border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs"> {{ step.label }} </div> <ul class="divide-border divide-y"> @for (row of step.rows; track row.name) { <li class="flex items-center justify-between gap-4 px-4 py-2.5"> <span class="min-w-0 truncate font-mono text-xs">{{ row.name }}</span> <span class="text-muted-foreground shrink-0 text-xs">{{ row.detail }}</span> </li> } </ul> </div> </div> </div> </ui-tabs-content> } </div> </ui-tabs> </div> </section> `, }) export class UiHowItWorksTabbedStepsComponent { protected readonly Check = Check protected readonly allSteps = steps @Input() set defaultStep(v: string) { if (steps.some((step) => step.id === v)) this.activeStep.set(v) } @Input('class') className?: string readonly activeStep = signal('connect') get rootClass(): string { return cn('bg-background', this.className) } pad(n: number): string { return String(n).padStart(2, '0') } setActiveStep(value: string): void { if (steps.some((step) => step.id === value)) this.activeStep.set(value) } }src/app/components/blocks/how-it-works-tabbed-steps/index.ts0.1 kB
export { UiHowItWorksTabbedStepsComponent, type TabbedStep, type TabbedStepRow, } from './how-it-works-tabbed-steps.component'
Raw manifest:https://uipkge.dev/r/angular/how-it-works-tabbed-steps.json