Getting started
Everything a product app needs to adopt Pho: install the package, wire the Tailwind theme, self-host the fonts, opt into dark mode, and connect your router. Five minutes end to end.
Install
pnpm add @photon-ai/pho-ui @tabler/icons-react
react / react-dom (18 or 19) and @tabler/icons-react are peer
dependencies; Base UI, Motion, and the class utilities come with the package.
Wire the theme
Pho is a Tailwind v4 theme plus components. In your app’s CSS entry, import
the tokens after Tailwind and point a @source at the shipped components so
their utility classes are generated:
@import "tailwindcss";
@import "@photon-ai/pho-ui/styles";
@source "../node_modules/@photon-ai/pho-ui/dist";
This gives you every pho-* semantic utility (bg-pho-page,
text-pho-secondary, ring-pho-brand, …), the accent palette
(--color-pho-yellow … --color-pho-navy), the squircle and
transition-inherit-all utilities, and the closed type system.
Two things are deliberately strict:
- The type scale is closed. Tailwind’s default sizes and weights are
reset — only
text-xs/base/md/lg/xl,text-display-xs/sm/base/md…2xl, andfont-light/normal/mediumgenerate utilities (font-mediumis 550 — UI, headings, and hero slogans; there is nofont-semibold). See Typeface. - No all-caps small text. Uppercase eyebrow/section-label treatments are banned; hierarchy comes from size, weight, and the alpha text ramp. See the casing rule on Typeface.
Fonts
The brand fonts are served from Photon’s asset CDN
(assets.photon.codes/pho-ui/v1/, immutable caching, CORS enabled) — one
import and they work, no files to copy:
@import "@photon-ai/pho-ui/styles/fonts";
To cut the first-paint swap, preload the primary face in your <head>
(crossorigin is required for fonts):
<link
rel="preload"
as="font"
type="font/woff2"
crossorigin
href="https://assets.photon.codes/pho-ui/v1/PolySansVF.woff2"
/>
Two escape hatches: if your app sets a font-src CSP, allow
https://assets.photon.codes; if you must self-host (air-gapped), skip the
import and declare the same faces against your own paths — the tokens only
reference the family names. PolySans is the voice (body 400, UI/headings/
hero slogans 550), Azeret Mono covers code and small labels, Instrument
Serif is the italic editorial accent.
Dark mode
Every color token resolves through CSS light-dark(), switched by
color-scheme. The stylesheet sets light on :root and dark on
[data-mode="dark"] and .dark — flip either on <html> (or any subtree)
and the whole system follows:
<html data-mode="dark">
<!-- everything inside renders with the dark values -->
</html>
Prefer the packaged segment — Device / Light / Dark. It flips data-mode,
follows the OS when Device is selected, persists under pho-mode, and keeps
multiple instances in sync:
import { ThemeToggle } from "@photon-ai/pho-ui/components/theme-toggle";
<ThemeToggle />;
Add a blocking script in <head> so the first paint matches storage:
<script>
const mode = localStorage.getItem("pho-mode");
const prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
if (mode === "dark" || ((mode === "device" || mode == null) && prefersDark))
document.documentElement.dataset.mode = "dark";
</script>
Using next-themes or a class-based switcher? Add a one-line bridge so your
class also flips the scheme:
.dark-mode {
color-scheme: dark;
}
Connect your router
ButtonLink, EffectButtonLink, and other navigation components render
through LinkProvider, so client-side routing works without Pho depending on
any router. Mount it once at the app root with your router’s link:
import { LinkProvider } from "@photon-ai/pho-ui/utils";
import { Link } from "react-router"; // or TanStack Router, Next.js, …
export function App({ children }) {
return (
<LinkProvider
component={({ href, ...props }) => <Link to={href} {...props} />}
>
{children}
</LinkProvider>
);
}
Without a provider, links fall back to a plain <a> — fine for MPAs and
prototypes.
Import components
Every component ships as its own entry for tree-shaking:
import { Button, ButtonLink } from "@photon-ai/pho-ui/components/button";
import { Input } from "@photon-ai/pho-ui/components/input";
import { Dialog } from "@photon-ai/pho-ui/components/dialog";
import { GoogleIcon, PhotonMark } from "@photon-ai/pho-ui/icons";
Prefer these subpath imports over the root barrel in app code — bundlers keep only what you use. Browse the full catalog under Components.
The look, in one paragraph
Pho is monochrome, straight from photon.codes: the app canvas is
bg-pho-page, content sits on bg-pho-primary cards with a
border-pho-secondary hairline and no shadow (elevation belongs to
floating layers only), buttons are pills with font-medium labels, the
primary action is the black/white contrast fill (bg-pho-brand-solid +
text-pho-on-brand), and ink is one ladder for copy and icons —
text-pho-primary (100%), text-pho-secondary (75%, UI chrome),
text-pho-description (55%, supporting prose). Color appears only as
functional states and the categorical accent set — see
Colors.