Skip to content

Campus UI

Campus UI is the semantic interface system for Campus-native applications. The gallery below renders the current components in context; use the patterns directly from plain HTML, JavaScript, React, or any framework that can emit accessible HTML.

Start with the stylesheet

Every Campus interface loads the content-addressed stylesheet published by the Campus UI manifest. During local authoring, use the stable alias; the package build rewrites it to the immutable URL.

html
<link rel="stylesheet" href="/assets/campus-app.css">

<main class="campus-page" data-campus-tint="intelligence">
  <!-- Campus UI components -->
</main>
js
const stylesheet = document.createElement('link')
stylesheet.rel = 'stylesheet'
stylesheet.href = '/assets/campus-app.css'
document.head.append(stylesheet)

document.documentElement.dataset.campusTint = 'intelligence'
jsx
export function CampusApp({ children }) {
  return (
    <main className="campus-page" data-campus-tint="intelligence">
      {children}
    </main>
  )
}

Buttons and status

Use semantic elements and Campus class names. The stylesheet owns touch sizing, tones, focus treatment, appearance, and responsive behavior.

html
<button class="campus-button campus-button-primary">
  <span class="campus-button-label">Deploy</span>
</button>

<span class="campus-status-pill campus-status-success">Ready</span>
js
function campusButton(label, tone = 'primary') {
  const button = document.createElement('button')
  button.type = 'button'
  button.className = `campus-button campus-button-${tone}`
  button.innerHTML = '<span class="campus-button-label"></span>'
  button.firstElementChild.textContent = label
  return button
}

actions.append(campusButton('Deploy'), campusButton('Cancel', 'secondary'))
jsx
function Button({ tone = 'primary', busy = false, children, ...props }) {
  return (
    <button
      type="button"
      className={`campus-button campus-button-${tone}`}
      aria-busy={busy || undefined}
      disabled={busy || props.disabled}
      {...props}
    >
      <span className="campus-button-label">{busy ? 'Working…' : children}</span>
    </button>
  )
}

<Button tone="tint">New project</Button>

Collections and data

Rows stay keyboard-accessible because the action is a native link or button. Compose identity, copy, metadata, and state without rebuilding row spacing in every app.

html
<section class="campus-collection" aria-label="Projects">
  <button class="campus-collection-row" aria-label="Open Research agent">
    <span class="campus-avatar" aria-hidden="true">AI</span>
    <span class="campus-row-copy">
      <span class="campus-row-eyebrow">INTELLIGENCE</span>
      <strong class="campus-row-title">Research agent</strong>
      <span class="campus-row-detail">Persistent tools and model access</span>
      <span class="campus-row-meta">Updated 2m ago</span>
    </span>
    <span class="campus-status-pill campus-status-success">Ready</span>
  </button>
</section>
js
function projectRow(project) {
  const row = document.createElement('button')
  row.type = 'button'
  row.className = 'campus-collection-row'
  row.setAttribute('aria-label', `Open ${project.name}`)
  row.innerHTML = `
    <span class="campus-avatar" aria-hidden="true"></span>
    <span class="campus-row-copy">
      <strong class="campus-row-title"></strong>
      <span class="campus-row-detail"></span>
    </span>`
  row.querySelector('.campus-avatar').textContent = project.initials
  row.querySelector('.campus-row-title').textContent = project.name
  row.querySelector('.campus-row-detail').textContent = project.detail
  return row
}
jsx
function ProjectRow({ project, onOpen }) {
  return (
    <button
      type="button"
      className="campus-collection-row"
      aria-label={`Open ${project.name}`}
      onClick={() => onOpen(project.id)}
    >
      <span className="campus-avatar" aria-hidden="true">{project.initials}</span>
      <span className="campus-row-copy">
        <strong className="campus-row-title">{project.name}</strong>
        <span className="campus-row-detail">{project.detail}</span>
      </span>
      <span className="campus-status-pill campus-status-success">Ready</span>
    </button>
  )
}

Forms and conversation

The same semantic markup works for settings, compose flows, and agent interfaces.

html
<label class="campus-field">
  Application name
  <input name="name" autocomplete="off">
  <span class="campus-field-help">Shown in Launcher and Store.</span>
</label>

<form class="campus-composer" aria-label="Message composer">
  <div class="campus-composer-main">
    <input name="message" aria-label="Message" placeholder="Ask the agent…">
    <button class="campus-icon-button" aria-label="Send message">↑</button>
  </div>
</form>
js
composer.addEventListener('submit', async (event) => {
  event.preventDefault()
  const data = new FormData(composer)
  await campus.backend.json('/messages', {
    method: 'POST',
    body: { text: data.get('message') },
  })
})
jsx
function Composer({ onSend }) {
  const [message, setMessage] = useState('')

  function submit(event) {
    event.preventDefault()
    if (!message.trim()) return
    onSend(message.trim())
    setMessage('')
  }

  return (
    <form className="campus-composer" aria-label="Message composer" onSubmit={submit}>
      <div className="campus-composer-main">
        <input value={message} onChange={(event) => setMessage(event.target.value)} />
        <button className="campus-icon-button" aria-label="Send message">↑</button>
      </div>
    </form>
  )
}

Sheets and confirmations

Use a Campus sheet for explicit confirmation, protected state, and workload-start boundaries. Avoid native confirm() and prompt().

html
<campus-modal-layer class="campus-sheet-layer" role="presentation">
  <section class="campus-sheet" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
    <header class="campus-sheet-header">
      <div>
        <span class="campus-eyebrow">CONFIRM ACTION</span>
        <h2 id="confirm-title">Start private workload?</h2>
        <p>This will use Campus Compute for this installation.</p>
      </div>
      <button class="campus-icon-button" aria-label="Close">×</button>
    </header>
    <div class="campus-form-actions campus-form-actions-end">
      <button class="campus-button campus-button-secondary">Cancel</button>
      <button class="campus-button campus-button-primary">Start workload</button>
    </div>
  </section>
</campus-modal-layer>
js
const dialog = document.querySelector('[role="dialog"]')
const close = () => dialog.closest('campus-modal-layer').remove()

dialog.querySelector('[aria-label="Close"]').addEventListener('click', close)
dialog.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') close()
})
dialog.querySelector('button').focus()
jsx
function ConfirmationSheet({ title, description, onCancel, onConfirm }) {
  return (
    <div className="campus-sheet-layer" role="presentation" onMouseDown={onCancel}>
      <section
        className="campus-sheet"
        role="dialog"
        aria-modal="true"
        aria-labelledby="confirm-title"
        onMouseDown={(event) => event.stopPropagation()}
      >
        <header className="campus-sheet-header">
          <div><h2 id="confirm-title">{title}</h2><p>{description}</p></div>
          <button className="campus-icon-button" aria-label="Close" onClick={onCancel}>×</button>
        </header>
        <div className="campus-form-actions campus-form-actions-end">
          <button className="campus-button campus-button-secondary" onClick={onCancel}>Cancel</button>
          <button className="campus-button campus-button-primary" onClick={onConfirm}>Confirm</button>
        </div>
      </section>
    </div>
  )
}

Current component surface

The gallery covers the visible output of the current library. The full exported surface is grouped below for quick reference.

AreaComponents
Structuregroup, app-page, page-header, section-header, toolbar, card
Collections and datacollection, collection-row, row-copy, avatar, metric-grid, metric, fact-list, fact
Actions and menusbutton, icon-button, panel-close-button, form-actions, action-menu, action-menu-item, action-popover, action-popover-item
Sheetssheet, sheet-header, confirmation-sheet, workload-action-sheet
Stateassurance-list, connection-status, protected-state, steps, loading-state, notice, banner, status-pill, badge, state-dot, empty-state, state-view
Conversation and commandsconversation, message, composer, search-field, command-surface, command-input, command-group, command-row, segmented-control
Forms and iconsfield, choice-field, microphone-icon, stop-icon, send-icon, panel-close-icon

Runtime contract

Campus UI emits accessible, semantic HTML and uses a shared content-addressed stylesheet. The stylesheet owns shell/content insets, safe areas, touch targets, light/dark appearance, mobile widths, desktop layouts, focus treatment, and common interaction states. App-local CSS should describe only composition and identity unique to that application.

Semantic tints are graphite for Campus/default, green for communication, orange for finance, blue for intelligence, cyan for files, and purple for OpenClaw-specific experiences.

For the source of truth, see frontend/src/campus/ui.cljs and the Campus UI design contract.

Software belongs to people. Campus gives it a durable place to run.