Before & After — Side by Side
blockmarketingTwo labelled panels set side by side at equal height with a shared annotation row beneath, for comparisons a slider would make harder rather than clearer.
Installation
$pnpm dlx uipkge-ng@latest add before-after-side-by-side$npx uipkge-ng@latest add before-after-side-by-side$yarn dlx uipkge-ng@latest add before-after-side-by-side$bunx uipkge-ng@latest add before-after-side-by-sideManifest:
https://uipkge.dev/r/angular/before-after-side-by-side.jsonInstalls to:src/app/components/blocks/before-after-side-by-side/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.
BeforeAfterRowinterface BeforeAfterRow {
text: string
note: string
}BeforeAfterPanelinterface BeforeAfterPanel {
label: string
caption: string
rows: BeforeAfterRow[]
}BeforeAfterAnnotationinterface BeforeAfterAnnotation {
id: string
text: string
}Files installed (2)
src/app/components/blocks/before-after-side-by-side/before-after-side-by-side.component.ts5.1 kB
// Two labelled panels set side by side at equal height with a shared annotation // row beneath, for comparisons a slider would make harder rather than clearer. // Port of the Vue/React block 1:1 — same Before/After file rows, same numbered // annotations referenced from both panels, same case-study CTA. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' 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 { cn } from '@/lib/utils' interface BeforeAfterRow { text: string note: string } interface BeforeAfterPanel { label: string caption: string rows: BeforeAfterRow[] } interface BeforeAfterAnnotation { id: string text: string } // Annotations are numbered and referenced from both panels, which is what makes // a side-by-side legible where a drag slider hides half the evidence. const panels: BeforeAfterPanel[] = [ { label: 'Before', caption: 'Four exports reconciled by hand', rows: [ { text: 'revenue_q1_final_v3.xlsx', note: '1' }, { text: 'revenue_q1_FINAL_v3b.xlsx', note: '1' }, { text: 'refunds_manual_adj.csv', note: '2' }, { text: 'fx_rates_pasted.csv', note: '3' }, ], }, { label: 'After', caption: 'One certified definition', rows: [ { text: 'metrics/revenue_net.yml', note: '1' }, { text: ' joins: refunds, fx_rates', note: '2' }, { text: ' owner: finance-analytics', note: '3' }, { text: ' reconciled: 4 quarters', note: '' }, ], }, ] const annotations: BeforeAfterAnnotation[] = [ { id: '1', text: 'Two files claiming to be final, differing by 40k in net revenue.' }, { id: '2', text: 'Refund adjustments applied by hand, with no record of who applied them.' }, { id: '3', text: 'FX rates pasted from a second system on a date nobody logged.' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-before-after-side-by-side, [ui-before-after-side-by-side]', standalone: true, // The block renders its own root <section>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiSeparatorComponent], template: ` <section data-slot="before-after-side-by-side" [class]="rootClass"> <div class="mx-auto max-w-5xl px-6 py-20 lg:py-28"> <span ui-badge variant="secondary">Before and after</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">The same close, two quarters apart</h2> <!-- items-stretch so both panels share a height; a slider would hide one side at a time and make the annotations unreadable. --> <div class="mt-10 grid items-stretch gap-4 md:grid-cols-2"> @for (panel of panels; track panel.label) { <div ui-card> <div ui-card-content class="flex h-full flex-col p-0"> <div class="border-border flex items-center justify-between gap-3 border-b px-4 py-2.5"> <span class="text-sm font-semibold">{{ panel.label }}</span> <span class="text-muted-foreground text-xs">{{ panel.caption }}</span> </div> <ul class="flex-1 space-y-2 p-4"> @for (row of panel.rows; track row.text) { <li class="flex items-start gap-2"> @if (row.note) { <span class="border-border text-muted-foreground mt-px flex size-4 shrink-0 items-center justify-center rounded-full border font-mono text-xs" > {{ row.note }} </span> } @else { <span class="size-4 shrink-0" aria-hidden="true"></span> } <span class="min-w-0 font-mono text-xs break-all">{{ row.text }}</span> </li> } </ul> </div> </div> } </div> <ui-separator class="my-8" /> <ol class="space-y-3"> @for (annotation of annotations; track annotation.id) { <li class="flex items-start gap-3 text-sm"> <span class="border-border text-muted-foreground mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border font-mono text-xs" > {{ annotation.id }} </span> <span class="text-muted-foreground leading-relaxed">{{ annotation.text }}</span> </li> } </ol> <button ui-button class="mt-8">Read the full case study</button> </div> </section> `, }) export class UiBeforeAfterSideBySideComponent { protected readonly panels = panels protected readonly annotations = annotations @Input('class') className?: string get rootClass(): string { return cn('bg-background', this.className) } }src/app/components/blocks/before-after-side-by-side/index.ts0.1 kB
export { UiBeforeAfterSideBySideComponent } from './before-after-side-by-side.component'
Raw manifest:https://uipkge.dev/r/angular/before-after-side-by-side.json