Pho Design System

Code Block

A read-only panel for a request body, a response, an event’s JSON, an install command. Mono text on the code surface, colored by Shiki with the grammar for its language, a title row with expand, wrap, and copy controls. Past maxLines the body scrolls inside that height; overflow="expand" keeps the old footer fold. Wide lines scroll sideways inside the panel; the page never does.

Event body

Import

import { CodeBlock } from "@photon-ai/pho-ui/components/code-block";

Languages

language names what the text is. Twenty-seven grammars — json, typescript, javascript, tsx, bash, python, swift, go, ruby, php, java, kotlin, csharp, rust, html, css, xml, yaml, toml, dotenv, sql, graphql, http, markdown, diff, jsonc, jsx — and the short names people reach for (ts, js, sh, py, yml, md). text, the default, colors nothing. A name the panel does not know reads as text.

Each grammar is fetched the first time a panel asks for it, and Shiki itself with the first grammar — a page of cards and rows never loads the engine. The body reads plain for the frame that takes, then the colors settle in; a grammar already here colors in the first render. Call preloadLanguage("ts") where you know a panel is coming.

notify.ts
import { Photon } from "@photon-ai/sdk";const photon = new Photon({ apiKey: process.env.PHOTON_API_KEY });// Send a text and wait for the delivery receipt.export async function notify(to: string, text: string): Promise<Receipt> {  const message = await photon.messages.send({ to, text, platform: "sms" });  return photon.receipts.wait(message.id, { timeout: 30_000 });}
<CodeBlock title="notify.ts" language="ts" code={source} />
<CodeBlock title="Event body" language="json" code={event} />
<CodeBlock language="bash" code="npm install @photon-ai/sdk" />

JSON

JSON has a synchronous face: a built-in tokenizer colors keys, strings, numbers, and the three keywords in the first render — on the server too — and the grammar, when it arrives, agrees with it token for token, so a body never flashes plain. A body that isn’t a string (an object) is pretty-printed with two-space indent.

Snippets

Without a title there is no title row. The controls float over the panel’s top-right corner, on the code surface, the way a command on a page wears its copy button.

npm install @photon-ai/sdk
curl -X POST https://api.photon.codes/v1/messages \  -H "Authorization: Bearer $PHOTON_API_KEY" \  -H "Content-Type: application/json" \  -d '{"to": "+15403908722", "text": "On its way."}'

Colors

Every scope a grammar knows routes to one of eight kinds, and each kind is one --color-pho-code-* token, which carries light and dark: key, string, number, keyword, punctuation, comment, function, type. Operators, variables, and everything else stay in the panel’s ink (fg, on bg). A body reads as prose with a few things picked out, not as confetti. The theme lives in the package; nothing to configure, no Shiki theme to pick.

Wrapping

Long lines scroll sideways by default, so a body reads the way it was sent. The toggle wraps them instead; defaultWrap starts wrapped, and wrap with onWrapChange owns it. wrapToggle={false} hides the control; the copy button stays.

Request headers
POST /v1/messages HTTP/1.1Host: api.photon.codesAuthorization: Bearer pho_sk_live_51Kk9fD3aB7cD2eF4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZIdempotency-Key: beab9fa1-f9e4-4908-b27a-7971ddc39c20Content-Type: application/json
Request headers
POST /v1/messages HTTP/1.1Host: api.photon.codesAuthorization: Bearer pho_sk_live_51Kk9fD3aB7cD2eF4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZIdempotency-Key: beab9fa1-f9e4-4908-b27a-7971ddc39c20Content-Type: application/json
<CodeBlock title="Request headers" code={headers} wrapToggle={false} />

Overflow

Past maxLines (30 by default) the body is clipped to that many lines and scrolls inside the panel. The title row’s expand toggle — same 32px squircle as wrap — reads Show all 58 lines; pressed, the panel grows to the full text and the label becomes Collapse to 24 lines. Copy still writes every line. The inner scroll hard-clips at the border; the ScrollArea edge fade stays off.

overflow="expand" keeps the footer button, Show all 58 lines, with a chevron that flips when the panel opens. Expanded, the button reads Show less. Use it when a reader should unfold rather than scroll.

Event body
{  "id": "pho_evt_615jeffmnmgpksp7t54c0jkwmj",  "apiVersion": "2026-07-01",  "type": "message.delivered",  "source": "sms.photon",  "timestamp": "2026-09-08T01:43:09.469Z",  "data": {    "object": {      "addressedTo": {        "details": {          "resourceId": "pho_res_01m04axjqrfzxv4xjet40a84gb"        },        "handle": "+15403908722",        "id": "pho_usr_3np2kr3q2ah8rb13crb4v3a8ap",        "kind": "agent",        "metadata": {},        "platform": "sms",        "projectId": "pho_prj_01kztbv929fagsn0pk422y682f"      },      "content": {        "format": "plain",        "metadata": {          "photon": {            "adaptations": []
<CodeBlock title="Event body" language="json" code={event} maxLines={24} />
<CodeBlock
  title="Event body"
  language="json"
  code={event}
  maxLines={24}
  overflow="expand"
/>

expandToggle={false} hides the header toggle and leaves the clipped scroll. The header order is expand, wrap, copy; each can be hidden.

Line numbers

lineNumbers draws a gutter. Off by default; turn it on for a long payload, about twenty lines or more. Numbers are not selectable, so a copy still writes only the code. A wrapped line keeps one number. The gutter sticks to the left when a long line scrolls sideways.

Event body
<CodeBlock
  title="Event body"
  language="json"
  code={event}
  maxLines={24}
  lineNumbers
/>

Empty

With no code the panel keeps its title and says empty in its place, Nothing to show. unless told otherwise. No copy, no wrap.

Request body

This request sent no body.

On a page

BasicPage.Code is this panel under the page’s name, placed in the stack as a block with its own rhythm. See Basic Page.

Props

Also exported, for a host that wants the tokens without the panel: highlight(code, language) resolves to lines of { type, text }, highlightSync does the same for a grammar already here, useHighlight is the hook the panel uses, preloadLanguage fetches a grammar early, CODE_TOKEN_CLASS maps a kind to its class, and tokenizeJson(line) is the synchronous JSON face.

Accessibility

Best practices