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:
- declare a page or UI surface
- ship app-owned CSS in
static/ - ship images or icons in
static/ormedia/ - reference those assets from templates
- 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, orDate, 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-counton 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_libsurface_kind: ui_lib
That row may declare either:
sectionsas the primary structured pathexamplesas 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_idstable identifier for the sectiontitlevisible section headingintrovisible guidance for the rendered sectionlayoutsection wrapper hint such asgridorstackexamplesarray of element declarations
Element declaration shape
Every UI element declaration uses the canonical identity and type contract:
idis the stable element identifier;example_idandelement_idare legacy and are not acceptedelement_typeselects the canonical element family, such asinput,select,textarea, orbuttoninput_typeselects the HTML input subtype whenelement_typeisinput; the legacywidgetfield is not acceptednameis 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:
inputtextarea
- text - email - number - time - date - search - url
Choice controls:
selectradio_groupcheckboxcheckbox_group
Display elements:
cardlistdetailtablebadge_liststat_group
Layout and action composition:
split_layoutcard_gridbutton_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
sectionsandexamples - 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 canonicalsh scripts/build_native.shsh 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.yamlapps/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.