Photon 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

  • code — string | object | null — the text; an object is printed as JSON
  • language — a grammar or alias, or text (default text)
  • title — ReactNode — the title row’s words; without one the controls float
  • maxLines — number (default 30) — the clip / fold height
  • overflow — scroll / expand (default scroll) — clip and scroll, or fold with a footer
  • wrap / defaultWrap / onWrapChange — wrapping, controlled or not
  • expandToggle — boolean (default true) — the header expand control (scroll only)
  • wrapToggle — boolean (default true) — the wrap control in the title row
  • lineNumbers — boolean (default false) — a gutter of line numbers
  • copy — boolean (default true) — the copy control
  • empty — ReactNode (default Nothing to show.)
  • actions — ReactNode — more controls beside the built-in ones, before them
  • …plus any <div> attribute

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

  • Text stays text. The body is a <pre><code>: selectable, searchable, read line by line. Colors are only how it looks.
  • Controls are named. Wrap long lines is a pressed / unpressed toggle; Copy request body (the title, lowercased; Copy code without one) announces Copied; the expand toggle is Show all N lines / Collapse to N lines with aria-pressed; the footer fold (overflow="expand") carries aria-expanded.
  • The scroll region is a region. When the body is clipped it is focusable, named after the title, and keeps a visible focus ring.

Best practices

  • Name the panel for what it holds: Request body, Response, Event body, notify.ts. Leave a one-line command untitled.
  • Say the language. text is for a body whose shape you don’t know.
  • Pass the object where you have one; the panel prints it. Pass the raw text where the shape matters (a body as sent).
  • Keep maxLines where a reader can still see there is more, twenty to forty. The default scroll keeps the page still; overflow="expand" is for a reader who should unfold.
  • Turn on lineNumbers for a long payload (about twenty lines or more).