Examples
Examples
Section titled “Examples”Button
Section titled “Button”import { useRef } from "react";import { Button } from "@fungi.computer/caps/components/button";
export function SaveButton({ pending, onSave,}: { pending: boolean; onSave: () => void;}) { const ref = useRef<HTMLButtonElement>(null);
return ( <Button ref={ref} type="submit" aria-label="Save changes" disabled={pending} loading={pending} variant="outline" size="sm" className="transition-none rounded-full" onClick={onSave} > Save </Button> );}The caller’s transition-none replaces the base transition utility, and
rounded-full adds a local treatment. daisyUI’s btn-outline and color classes
compose, so color belongs in variant or an extra daisyUI class.
Composer with an async submit
Section titled “Composer with an async submit”Composer is a layout block. Its input and action are your controls, and so are
their labels, disabled state, keyboard behaviour and effects. An avatar or other
leading content goes in the leading slot as any React node.
import { useState, type FormEvent } from "react";import { Button } from "@fungi.computer/caps/components/button";import { Composer } from "@fungi.computer/caps/blocks/composer";
export function PromptComposer({ submit,}: { submit: (value: string) => Promise<void>;}) { const [value, setValue] = useState(""); const [pending, setPending] = useState(false);
async function onSubmit(event: FormEvent) { event.preventDefault(); if (!value.trim() || pending) return; setPending(true); try { await submit(value); setValue(""); } finally { setPending(false); } }
return ( <form onSubmit={onSubmit}> <Composer input={ <input className="input join-item w-full" value={value} onChange={(event) => setValue(event.target.value)} aria-label="Prompt" /> } action={ <Button type="submit" size="sm" disabled={pending || !value.trim()} loading={pending} > Send </Button> } /> </form> );}submit is your effect. It can call an API, enqueue work or do something local.
Caps only joins the controls.
Window
Section titled “Window”import type { ReactNode } from "react";import { WindowActions, WindowBody, WindowFrame, WindowHeader,} from "@fungi.computer/caps/components/window";
export function NotesWindow({ active, close, children,}: { active: boolean; close: () => void; children: ReactNode;}) { return ( <WindowFrame aria-label="Notes" active={active}> <WindowHeader title="Notes" actions={<WindowActions title="Notes" onClose={close} />} /> <WindowBody>{children}</WindowBody> </WindowFrame> );}App bar
Section titled “App bar”AppBar keeps running apps visible to the left of an anchored launcher.
Clicking the launcher reveals available apps to the right and an optional
primary action above. You own app identity, icons, selected state and callbacks.
Caps owns disclosure, Anime.js springs, overflow and keyboard focus.
import { AppBar } from "@fungi.computer/caps/blocks/app-bar";
export function Dock({ openWindow, openSettings, createChat,}: { openWindow: (id: string) => void; openSettings: () => void; createChat: () => void;}) { return ( <AppBar windows={[ { id: "editor", label: "Editor", active: true, onSelect: () => openWindow("editor"), }, ]} launchers={[ { id: "settings", label: "Settings", onSelect: openSettings }, ]} launcher={{ label: "Open apps" }} primaryAction={{ label: "New chat", icon: <span aria-hidden="true">+</span>, onSelect: createChat, }} /> );}Icons are React nodes. Narrow screens keep the same two scrollable trays. Escape
closes the disclosure and returns focus to the launcher. Reduced motion settles
immediately. Available-app and primary-action buttons do not claim toggle state.
alwaysExpanded keeps the disclosure open for dock layouts.
Status beads and dashboard controls
Section titled “Status beads and dashboard controls”Status is a gooey SVG bead. Set glyph (dot, pill or bang), tone, and
optional motion (none, pulse or bounce). Shape changes animate from the
current geometry with Anime.js, and reduced motion settles immediately. You map
your state to these props. An optional label renders beside the decorative
bead.
import { Status } from "@fungi.computer/caps/components/status";
export function InboxBead({ unread, selected,}: { unread: boolean; selected: boolean;}) { return ( <> <Status glyph={unread ? "bang" : selected ? "pill" : "dot"} tone="primary" /> <Status label="Working" tone="success" motion="pulse" /> </> );}IconButton defaults to a ghost, circular, extra-small control. It keeps its
grid size on phones and touch screens, and an invisible hit area reaches
--caps-touch-target instead. A disabled ghost button stays faceless and dims
to --caps-disabled-ink. Buttons with a face keep the outlined plate.
ScrollToBottomBlob observes a column-reverse scroller you supply, and you own
the jump callback.
Status tray
Section titled “Status tray”@fungi.computer/caps/blocks/status-tray exports StatusTray,
StatusTrayList, StatusTrayRow and StatusTrayNote. You supply readouts and
content, own openId and handle actions. One tray opens at a time. Escape and
the close control return focus to its chip. Outside pointer presses close it
without stealing focus. Disabled items never open, and zero counts stay hidden.
import { useState } from "react";import { StatusTray } from "@fungi.computer/caps/blocks/status-tray";
export function PortsTray({ ports }: { ports: readonly string[] }) { const [openId, setOpenId] = useState<string | null>(null); return ( <StatusTray openId={openId} onOpenChange={setOpenId} items={[ { id: "ports", label: "Ports", count: ports.length, tone: "success", content: ( <ul> {ports.map((port) => ( <li key={port}>{port}</li> ))} </ul> ), }, ]} /> );}The tray is built from Caps parts. A Popover is the housing, a Display is
the inset screen, Status is each LED and row actions are KeycapButtons
(tone="primary" for the one accented key, tone="destructive" for a quiet
Stop that turns red on hover or focus). StatusTrayList puts one Display
behind its rows and seats each row’s keys on the bezel beside it.
StatusTrayNote is a short sentence on its own Display. Give it a title for
an unavailable feed (“No signal”), which is not a zero count.
leading and trailing seat adjacent controls, and an item’s after places
controls right after its chip in DOM and keyboard order. align chooses the
panel edge. placement="screen" uses flat screen-native readouts instead of
housing chips. Themes can set --caps-status-tray-chip-*,
--caps-status-tray-count-bg, --caps-status-tray-title-{case,tracking},
--caps-status-tray-width, --caps-status-tray-key-{face,tint,height},
--caps-status-tray-readout-{font,divider}, --caps-status-tray-row-padding,
--caps-status-tray-action-gap, --caps-status-tray-row-wrap,
--caps-status-tray-chips-display, --caps-status-tray-note-{height,tracking},
--caps-status-tray-muted and
--caps-status-tray-screen-{bg,fg,bg-active,fg-active}. Defaults use Stipe
tokens.
Game pieces
Section titled “Game pieces”Game UI uses the same plastic material as the rest of Caps, plus the ink line
that pixel art is drawn with. --caps-ink resolves to a theme’s --stipe-ink,
or to a near-black line in light themes and the deepest base in dark ones.
import { KeycapButton } from "@fungi.computer/caps/components/keycap";import { GameMeter } from "@fungi.computer/caps/components/progress";import { PixelNumber } from "@fungi.computer/caps/components/pixel-number";import { ItemGrid, type ItemGridCell, type ItemGridGhost, type ItemGridItem,} from "@fungi.computer/caps/components/item-grid";
export function Hud({ building, toggleBuild, items, ghost, pickUpOrDrop, previewDrop,}: { building: boolean; toggleBuild: () => void; items: readonly ItemGridItem[]; ghost: ItemGridGhost | null; pickUpOrDrop: (cell: ItemGridCell) => void; previewDrop: (cell: ItemGridCell | null) => void;}) { return ( <> <KeycapButton label="Build" shortcut="B" pressed={building} onClick={toggleBuild} /> <GameMeter label="Hull" value={7} max={10} tone="success" /> <PixelNumber text="08:00" size="lg" tone="primary" /> <PixelNumber value={3} signed outline tone="success" /> <ItemGrid label="Backpack" columns={8} rows={6} items={items} ghost={ghost} onCellActivate={pickUpOrDrop} onCellHover={previewDrop} /> </> );}Keycapis a nativekbd.KeycapButtonis a native button whoseshortcutonly labels the key. Passingpressedmakes it a toggle with a lamp andaria-pressed. Your app owns the key binding.toneis a role:primaryandaccenttint the face from the theme colour,inkis the dark key, anddestructivestays neutral until hovered or focused.label={null}makes an icon-only key that needs anaria-label. The molded lip, sheen and press travel scale with--depth. Themes set--caps-keycap-{face,ink,edge,tint,width,min-width,height,justify,padding,font-size,font-weight}.GameMeterpairs a labelled native progress bar with the current and maximum values. The game supplies both values and the tone.PixelNumberdraws a 3×5 face as SVG at integer scales and keeps the plain text for assistive tech.outlinerings the lit pixels in the ink line so the number reads over any world.ItemGridis presentation. Cells carry the keyboard and pointer interface (arrows, Home and End, Enter and Space), and items and the drop ghost are drawn from your facts. It never moves, merges or validates an item. A refused ghost shows your reason under the tray and describes the grid with it.ItemSlotis one paper-doll pocket.
Editorial pieces
Section titled “Editorial pieces”import { Keycap, StatusLed } from "@fungi.computer/caps/components/field-guide";import { Folio, Masthead, SectionBar,} from "@fungi.computer/caps/components/technonomicon";
export function FrontPage() { return ( <> <Masthead href="/" wordmark="The Spore Print" tagline="Notes from a very strange forest." edition={["Saturday, September 26, 2026", "Issue I"]} sections={ <SectionBar aria-label="Sections" sections={[{ href: "/", label: "Front Page", current: true }]} /> } /> <p> Press <Keycap>J</Keycap> for the next story.{" "} <StatusLed active label="Online" /> </p> <Folio items={["The Spore Print", "Page A1"]} /> </> );}On a server-rendered page, turn on card flips and pencil marks once in the browser:
import { enhanceCigaretteCards } from "@fungi.computer/caps/enhance/cigarette-card";import { sketch } from "@fungi.computer/caps/enhance/sketch";
enhanceCigaretteCards();sketch();