---
title: Showcase
description: A component at every width it has to survive, with its source, install commands, and a live StackBlitz editor.
source: https://adamjurek.com/components/showcase
---

## Home


The frame below is the component this page documents, showing another
component. Switch the width - the layout changes because the preview really is
a different viewport.

<!-- ::start:showcase demo="card" height="380" -->
<!-- ::end:showcase -->

## Why an iframe

Because a resized `div` lies.

A div at 390px still inherits the page's viewport, so `@media (max-width: 860px)`
never fires inside it. A component can look perfect in a showcase built that way
and break on an actual phone. An iframe has its own viewport, so the media
queries that run are the real ones.

## What else it shows

| Control | Does |
| --- | --- |
| Preview / Code / StackBlitz | the running component, the source, or a live editor |
| Install rows | the TanStack and shadcn commands, attached automatically |

> [!NOTE] Install commands are not written by hand
> Anything in the registry gets its commands attached from its registry entry,
> so "how do I get this" is never something an author has to remember.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/showcase.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/showcase.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | docs · Presentation |
| Files | `showcase.tsx` |
| Dependencies | None |
| Also installs | `icon`, `copy-button`, `device`, `use-device-kind` |
| Tags | iframe, responsive |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Showcase } from "@sushindustries/ui";

export function Example() {
	return (
		<Showcase
			src="/preview/card"
			title="Card"
			code={`<Card>...</Card>`}
			install={{ tanstack: "pnpm dlx @tanstack/cli add card" }}
		/>
	);
}
```

`src` has to point at a real route that renders the component alone, with
nothing else on the page - that route is the app's to build, not something
this component generates.

## What you should see

A bar with Preview and Code tabs, a row of width buttons, and Compare
selected by default: four to six framed iframes side by side, each labelled
with its width and why that width was picked. Switch to Code and the same
component renders again next to its source, with a copy button over the
block. If `install` was passed, a row of copyable commands sits under
whichever tab is open.

## If nothing happens

The frames stay blank when `src` does not resolve to a real page - a typo in
the route, or a preview page that was never built for this component. Check
the Preview tab first with the browser's own devtools open on the iframe; a
404 inside the frame looks identical to an empty component from the outside.


## Guides


## The widths

Not a rounded-off guess at popular phones. Each width sits on one side of a
breakpoint this stylesheet actually contains, so the set exercises every branch
in it and nothing else.

| Width | Why that number |
| --- | --- |
| 320 | the floor. Every component here works from this width up |
| 390 | the commonest real phone, still under the 860px breakpoint |
| 900 | between 860 and 1080: past the phone layout, short of the wide one |
| Desktop | whatever the page has. Pinning it to 1280 misreports a laptop |

Each frame says which width it is and why. A frame with no label is a
screenshot; a labelled one is a claim you can check.

## Compare

**Compare** puts all four side by side in a row that scrolls.

One width at a time answers "does it work here", which is usually the question
you already know the answer to. All of them at once answers "where does it stop
working", which is the one worth a screenful. The frames align to the top, so a
short component does not stretch its frame to match the tallest one and hide
the fact that it was short.

```css
.showcase-stage {
	display: flex;
	align-items: start;
	justify-content: center;
}

/* Compare is a row that scrolls. One width at a time centres instead. */
.showcase-stage[data-view="compare"] {
	justify-content: start;
	overflow-x: auto;
}
```

There is no transition on the device toggle. It gets pressed a dozen times
while reading one page, and on a control used that often an animation reads as
lag rather than as polish - the state change is the feedback.

## StackBlitz

The **StackBlitz** tab opens a live, editable copy of the demo in a real
WebContainer. The reader can change the code and see the result without leaving
the page.

The project is built from the same source the Code tab shows - the demo's
`source` string becomes `src/Demo.tsx` in a React + TypeScript project that
imports `@sushindustries/ui` and `@sushindustries/atoms`. So the editable copy
is the same code the reader was just looking at, not a reconstruction of it.

The StackBlitz SDK is wired in the app layer, not in the Showcase component
itself, for the same reason the code highlighter is: `packages/ui` has no
business depending on the StackBlitz SDK. The Showcase component takes a
`renderStackblitz` render prop and decides where it goes; the host builds the
project and hands it to the SDK.

```tsx
<Showcase
	src={`/preview/${id}?fit=full`}
	code={code}
	language={language}
	renderStackblitz={(source, lang) => (
		<StackblitzEmbed demoId={id} code={source} language={lang} />
	)}
/>
```

The tab only appears when both `code` and `renderStackblitz` are given. Leave
either off and the reader gets Preview and Code, which is the right thing for a
demo nobody can usefully edit.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `src` | `string` | - | URL of a bare page rendering just the component. |
| `title?` | `string` | - |  |
| `code?` | `string` | - | Source of the example, shown under the Code tab. |
| `language?` | `string` | `"tsx"` | Language for the code fence. |
| `install?` | `Readonly<Record<string, string>>` | - | Install commands, keyed by installer name. |
| `installLogos?` | `Readonly<Record<string, string>>` | - | Installer logos, keyed by the same names. Their marks, quoted as images. |
| `height?` | `number` | `420` | Height of the desktop frame, which has no device height of its own. |
| `renderCode?` | `(code: string, language: string) => ReactNode` | - | Rendered code block. Passed in so this file needs no highlighter. |
| `renderStackblitz?` | `(code: string, language: string) => ReactNode` | - | Renders the StackBlitz embed for the Code tab. Passed in for the same reason as `renderCode`: this package has no business depending on the StackBlitz SDK. The host builds a project from the demo's source and hands it to the SDK; this component only decides where it goes and when it is visible. |

<!-- /generated:api -->

## Notes

Three tabs, and each one needs a pair of props before it shows at all. The
Code tab needs both `code` and `renderCode` - either alone and the tab does
not appear, because there is nothing to render or nothing to render it with.
The StackBlitz tab needs `code` and `renderStackblitz` the same way, and
shares the `code` prop the Code tab uses rather than taking its own.

`install` and `installLogos` are keyed by the same installer names -
`install.tanstack` pairs with `installLogos.tanstack`. A name present in one
and not the other still renders; it just shows a command with no mark beside
it, or a mark with nothing to run.

The Preview tab opens on Compare, not on one device width. Every width in the
row is a live iframe of the real component, not a screenshot, so "does it
work here" and "where does it stop working" are the same tab - the reader
scrolls to the second question instead of clicking to reach it.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="showcase" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

This is the shape every component page on this site uses under its own
`::start:showcase` block - `src` is a per-component preview route, `code` is
the string shown on the Code tab, and `renderCode` / `renderStackblitz` are
supplied once by the app rather than by every call site.

```tsx
import { Showcase } from "@sushindustries/ui";
import { highlight } from "~/modules/code/highlight";

export function ComponentPreview({ slug, source }: { slug: string; source: string }) {
	return (
		<Showcase
			src={`/preview/${slug}`}
			title={slug}
			code={source}
			renderCode={(code, language) => highlight(code, language)}
			height={380}
		/>
	);
}
```

## What this example is not

`renderCode` and `renderStackblitz` are render props for a reason: this
package ships no syntax highlighter and no StackBlitz SDK. A host that skips
`renderCode` still gets a working Code tab - the fallback is a plain `<pre>`
- and skipping `renderStackblitz` just drops that tab rather than breaking
anything.
