Overview
Start Here
Installation
Tutorial
Architect App Builder
Todo List App
Beacon Beats Audio Store
UCAL App
Authoring
Dynamic Apps
Static Apps
dev_docs
Why Pharos Feels Different
Architect Builder
Framework
Artifacts & Layout
Configuration
Outbound Email
How to Use CLI
Contracts
YAML Authoring
Action Hooks and Middleware
Feature Transaction Primitives
Generated Surfaces
Declarative CRM
Input Validation
Identity and Tenancy
UI Surface Library
Social Connections
Payment Provider Management
Integration Adapters
Reference
Contract, UI, and Surface Reference
Styling UI Elements
Architect Media Artifacts
API Docs
Use Cases
Dynamic SPAs
Generated Admin and APIs
Scheduling and Marketplaces
Multi-App Hosting
Release Notes
Vision
Pharos Framework

← Back

Styling UI Elements

Style visual elements in a Pharos app with CSS, image assets, and icons while keeping behavior in the shared runtime and presentation in app-owned declarative files.

This page also covers the framework-owned UI element library. The same authoring pass usually needs both the element contract and the styling boundary, so they are documented together here.

Combined authoring model

  • declare the UI element surface in app-owned fixture data
  • let Pharos render the shared semantics
  • style the result with app-owned CSS and assets
  • keep behavior in action, route, workflow, and integration contracts

Styling model

  • request handling, rendering semantics, logging, workflow execution — shared native runtime
  • page declaration, templates, CSS, images, icons, copy — app-owned files
  • UI element semantics — shared runtime
  • visual theme and layout treatment — app-owned CSS

That means the normal customization path is:

  1. declare a page or UI surface
  2. ship app-owned CSS in static/
  3. ship images or icons in static/ or media/
  4. reference those assets from templates
  5. let Pharos keep owning behavior

Where styling lives

A typical app-owned styling layout looks like this:

apps/<app_id>/
  static/
    css/
      site.css
    img/
      logo.svg
      hero.jpg
    icons/
      mark.svg
  templates/
    layout/base.html
    home.page.yml
    home.body.html

The checked dynamic_app app already demonstrates the baseline pattern:

  • stylesheet link in apps/dynamic_app/templates/layout/base.html
  • app CSS in apps/dynamic_app/static/css/site.css
  • app-owned HTML structure in apps/dynamic_app/templates/home.body.html

Linking a stylesheet

Add your stylesheet from the base layout or page layout template:

<!doctype html>
<html lang="{current_lang}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{title}</title>
  <link rel="stylesheet" href="/static/css/site.css">
</head>
<body>
  <main>
    {html:body}
  </main>
</body>
</html>

CSS patterns that already work in Pharos

The shipped dynamic template uses plain, portable CSS classes instead of framework-specific JS or runtime hooks.

Layout wrappers

Use lightweight layout classes to shape the page around framework-rendered elements.

main {
  max-width: 1080px;
  margin: 0 auto;
  padding: 2rem 1rem 3rem;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.card {
  background: #fff;
  border: 1px solid #dbe4ee;
  border-radius: 0.9rem;
  padding: 1rem;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}

Compact interaction defaults

Use compact, predictable control layout across generated and hand-authored apps:

  • Text inputs and textareas that already contain clear helper or placeholder text should use accessible hidden labels instead of duplicating a visible label above the field.
  • Prefer icon buttons with title, aria-label, and fast hover or focus text for navigation, reload, close, delete, save, and other compact actions.
  • Date and time controls should be sized to the expected value rather than stretching across the full row. Put a short visible label on the left, usually At, Time, or Date, and keep the control close to that label.
  • Cards should put their first-line identity, status, and secondary actions together in a compact header row. Put the primary save, submit, order, or confirm action in the bottom-right action row when the card has a footer action.
  • Use horizontal button bars for peer footer actions and vertical button bars for dense side actions. Vertical bars default to 32 by 32 pixel icon buttons and set --button-count on the bar so the bar height is calculated from the number of buttons.
  • Popups, dialogs, drawers, and side panels should put close or back on the first line, right-aligned, and keep the main action in the bottom-right line with related status or price metadata.

Page layout archetypes

Generated apps should choose a whole-page layout before choosing individual cards or controls. The current app set demonstrates these page-level patterns:

  • Hero/grid index pages for public home screens, catalogs, note grids, and capability overviews. Use a clear hero or masthead followed by a responsive grid and a separate empty state.
  • Settings console pages for setup and readiness workflows. Keep the page title outside cards, put the primary configuration card wide, and place supporting status cards below it.
  • Master/detail workspaces for queues, records, carts, owners, and admin pages. Pair the collection with a selected detail or side panel and keep each action tied to a bounded target.
  • Calendar matrix pages for scheduling. Put the toolbar first, then the day/time grid, then appointment detail and action panels.
  • Checkout or transaction side panels for cart, order, approval, export, and payment-like flows. Put close/back in the first line and the confirm or download action at the bottom right.
  • Documentation shells for reference apps. Use persistent navigation, stable anchors, code/example cards, and preview sections rather than transactional controls.

These archetypes are indexed from app-owned templates and CSS so Architect can reuse page mechanics from existing apps as the app library grows.

Editable select option managers

Use an editable select option manager when a generated app needs reusable values such as KPI categories, metric types, lead stages, appointment types, product categories, workflow states, or tags. The control should render a select for the current value plus compact add and delete icon buttons tied to declared actions that update the option collection and return the same HTMX target region.

Do not render inert add/delete icons beside a select. If option management actions are not declared yet, use a normal select with safe default values and include the option-manager requirement in the generated acceptance notes.

For KPI and dashboard apps, seed useful category and metric-type options from the prompt and known dashboard patterns instead of starting with blank text boxes. Common defaults include Financial Health, Customer & Support, Sales & Marketing, Operations & Productivity, User Acquisition, Engagement & Usage, Retention & Loyalty, and Technical Performance.

Vertical icon button bars

Use a vertical button bar when a card, popup, or side panel needs several compact actions but horizontal space belongs to the content. Set --button-count to the number of buttons so the bar reserves the correct height. The default button size is 32 by 32 pixels.

<div class="vertical-button-bar" style="--button-count: 3" aria-label="Record actions">
  <button class="icon-action-button" type="button" title="Edit" aria-label="Edit"><span class="bi bi-pencil-fill icon-action-button__icon" aria-hidden="true"></span><span class="icon-action-button__tooltip">Edit</span></button>
  <button class="icon-action-button" type="button" title="Save" aria-label="Save"><span class="bi bi-floppy-fill icon-action-button__icon" aria-hidden="true"></span><span class="icon-action-button__tooltip">Save</span></button>
  <button class="icon-action-button" type="button" title="Delete" aria-label="Delete"><span class="bi bi-trash-fill icon-action-button__icon" aria-hidden="true"></span><span class="icon-action-button__tooltip">Delete</span></button>
</div>
.vertical-button-bar {
  --button-count: 1;
  --button-size: 32px;
  --button-gap: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: var(--button-gap);
  inline-size: var(--button-size);
  block-size: calc((var(--button-count) * var(--button-size)) + ((var(--button-count) - 1) * var(--button-gap)));
}

Forms and controls

The template app also demonstrates a good baseline for app-owned form styling:

.form-control {
  width: 100%;
  margin-top: 0.35rem;
  padding: 0.65rem 0.75rem;
  border: 1px solid #cbd5e1;
  border-radius: 0.55rem;
  background: #fff;
  box-sizing: border-box;
}

.ui-button {
  padding: 0.7rem 1rem;
  border-radius: 0.6rem;
  border: 1px solid #0f172a;
  background: #0f172a;
  color: #fff;
  font-weight: 600;
}

Grid composition for framework-owned UI surfaces

Framework-rendered UI library elements can still be styled at the page level by targeting their shared wrapper classes.

.ui-library-grid,
.ui-card-grid,
.ui-stat-grid,
.ui-split-layout {
  display: grid;
  gap: 1rem;
}

.ui-library-grid {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.ui-card-grid {
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

Responsive behavior

Keep responsive behavior in CSS instead of trying to fork runtime behavior by device type.

@media (max-width: 700px) {
  main {
    padding: 1.25rem 0.85rem 2rem;
  }

  .task-form {
    flex-direction: column;
    align-items: stretch;
  }
}

Styling the framework-owned UI element library

Pharos owns the HTML semantics of ui_lib examples, but your app owns the surrounding CSS.

UI element surface contract

The canonical authored source is:

  • apps/dynamic_app/data/contracts/ui-elements.yml

The compiled runtime artifact is emitted under the built app output as JSON.

The reference surface row is:

  • surface_id: dynamic_app_ui_lib
  • surface_kind: ui_lib

That row may declare either:

  • sections as the primary structured path
  • examples as the compatibility fallback path

Section shape

A section groups related element examples together.

section_id: inputs_and_widgets
title: Inputs and widgets
intro: Use these controls when a declarative action needs typed operator input without
  custom runtime glue.
layout: grid
examples: []

Section fields:

  • section_id stable identifier for the section
  • title visible section heading
  • intro visible guidance for the rendered section
  • layout section wrapper hint such as grid or stack
  • examples array of element declarations

Element declaration shape

Every UI element declaration uses the canonical identity and type contract:

  • id is the stable element identifier; example_id and element_id are legacy and are not accepted
  • element_type selects the canonical element family, such as input, select, textarea, or button
  • input_type selects the HTML input subtype when element_type is input; the legacy widget field is not accepted
  • name is the submitted field name for form controls

Declarations may also carry presentation data such as title, label, usage, tooltip, options, rows, cards, panels, or buttons.

Current element types

Inputs and text capture:

  • input
  • - text - email - number - time - date - search - url

  • textarea

Choice controls:

  • select
  • radio_group
  • checkbox
  • checkbox_group

Display elements:

  • card
  • list
  • detail
  • table
  • badge_list
  • stat_group

Layout and action composition:

  • split_layout
  • card_grid
  • button_row

Ownership boundary

Apps own:

  • which sections exist
  • which examples are shown
  • labels, usage text, tooltips, and visible copy
  • option lists and record-like demo values
  • canonical control fragments under apps/<app_id>/templates/shared/fragments/ui_*.html

Pharos owns:

  • HTML rendering
  • element semantics
  • shared layout wrappers
  • compatibility behavior between sections and examples
  • the visible contract used by the live dynamic app and future framework-owned surfaces

Raw <input>, <select>, <textarea>, and <button> markup must live only in those app-owned canonical UI fragments. Page bodies and feature-specific fragments should compose those fragments rather than duplicate them.

Current shared UI classes in the shipped template

  • .ui-library-section — top-level section wrapper
  • .ui-library-grid — grid layout for examples
  • .ui-card-grid — grid of summary cards
  • .ui-stat-grid — stat summary layout
  • .ui-split-layout — two-panel composition
  • .ui-example-card — wrapper for one rendered example
  • .ui-table — framework-rendered table
  • .ui-badge-list — badge container
  • .ui-badge — individual badge
  • .ui-stat-card — metric card
  • .ui-button-row — grouped actions
  • .ui-button-primary — primary action treatment
  • .ui-button-secondary — secondary action treatment
  • .ui-button-quiet — low-emphasis action treatment

Example: restyling cards and badges

.ui-example-card,
.ui-stat-card,
.card {
  background: linear-gradient(180deg, #ffffff, #f8fafc);
  border: 1px solid #d9e2ec;
  border-radius: 1rem;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}

.ui-badge {
  background: #ede9fe;
  color: #5b21b6;
  border: 1px solid #c4b5fd;
}

Example: product-themed action buttons

:root {
  --brand-600: #0f766e;
  --brand-700: #115e59;
  --brand-100: #ccfbf1;
  --line-soft: #99f6e4;
}

.ui-button-primary {
  background: var(--brand-600);
  border-color: var(--brand-700);
  color: #ffffff;
}

.ui-button-secondary {
  background: var(--brand-100);
  border-color: var(--line-soft);
  color: var(--brand-700);
}

.ui-button-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--brand-700);
}

Using images and icons

Put image and icon assets in app-owned files and reference them from templates.

Example: hero image

<section class="hero">
  <img src="/static/img/hero.jpg" alt="Business dashboard preview">
  <div>
    <h1>{title}</h1>
    <p>{intro}</p>
  </div>
</section>

Example: icon inside a button

<button type="submit" class="ui-button ui-button-primary">
  <img src="/static/icons/check.svg" alt="" aria-hidden="true">
  Approve
</button>

Example: background image in CSS

.hero {
  background-image: url("/static/img/hero.jpg");
  background-size: cover;
  background-position: center;
}

Safe styling boundaries

Do

  • style runtime-owned elements with app-owned CSS classes and selectors
  • add images, icons, and fonts from app-owned static assets
  • change spacing, color, borders, typography, and layout in CSS
  • wrap framework-owned elements in app-owned page structure

Do not

  • move request handling into templates
  • implement runtime behavior in CSS or embedded scripts
  • depend on JS-only widget libraries to replace framework-owned form semantics
  • treat templates as a substitute execution layer for routing, persistence, or workflow logic

Practical pattern

  • button color, card spacing, typography, badges, responsive layout — app CSS
  • hero image, icon set, logo — app static assets
  • form submission, validation contract, mutation behavior — Pharos action and form fixtures
  • rendered UI example semantics — Pharos shared runtime

Validation path

Use the dynamic app as the reference implementation:

  • scripts/check.sh ui canonical
  • sh scripts/build_native.sh
  • sh scripts/check.sh dynamic-app native-runtime

The canonical UI check is a source-policy check. The dynamic app runtime check proves the rendered result.

Generated-admin crossover

The same UI surface also feeds generated-admin composition.

Generated-admin surfaces can point at app-owned composition files under generated_admin/, for example:

  • apps/ucal/generated_admin/service.yaml
  • apps/ucal/generated_admin/business_availability.yaml

Those files use the JSON-compatible YAML subset so the native runtime can load them directly while keeping the source app-owned.

See also

  • Pharos app surface reference
  • Generated surfaces