Component Library
Server-rendered building blocks. Each component is a pure function that returns an HTML string — no client-side JS, no build step, accessible and mobile-ready by default. The output is the same every time the same props are passed.
Setup
Components are imported directly into the spec file alongside the stylesheet reference in meta.styles. No build step, no registration.
import { button, card, input } from '@invisibleloop/pulse/ui'
import { escHtml } from '@invisibleloop/pulse/html'
export default {
route: '/example',
meta: {
title: 'Example',
styles: ['/pulse-ui.css', '/app.css'],
},
view: () => `
<main id="main-content">
${card({
title: 'Welcome',
content: button({ label: 'Get started', href: '/start' }),
})}
</main>
`,
}
Theming
All visual values flow through CSS custom properties. Override any --ui-* token in :root in your app.css to retheme the entire library at once.
/* app.css */
:root {
--ui-accent: #6366f1;
--ui-accent-hover: #818cf8;
--ui-accent-dim: rgba(99, 102, 241, .12);
--ui-radius: 6px;
--ui-bg: #ffffff;
--ui-surface: #f9fafb;
--ui-text: #111827;
--ui-muted: #6b7280;
--ui-border: #e5e7eb;
}
New variants follow the ui-btn--{name} CSS modifier pattern. A brand-coloured button, for example, is just a new class in app.css — the component itself stays untouched.
.ui-btn--brand {
background: var(--brand);
color: #fff;
border: none;
}
.ui-btn--brand:hover:not(.ui-btn--disabled) {
background: var(--brand-hover);
}
Components
Each component has its own page with full demos, code examples, and a props reference.
UI
| Component | Description |
|---|---|
| button | Renders as <button> or <a>. Four variants, three sizes. |
| badge | Inline status label. Five semantic colour variants. |
| card | Content surface with title, body, and optional footer. |
| input | Labelled text input with hint and error support. |
| fieldset | Semantic grouping of related fields with an accessible legend. |
| select | Styled select with option groups and current-value support. |
| textarea | Multi-line input with label, hint, and error. |
| alert | Inline feedback banner. ARIA roles wired by variant. |
| stat | Numeric metric with optional trend arrow. |
| avatar | User avatar — image with fallback to initials. |
| empty | Empty state with title, description, and optional CTA. |
| table | Accessible data table with scroll wrapper. |
Landing page
| Component | Description |
|---|---|
| nav | Site header with logo, links, and optional CTA. |
| hero | Full-width hero section with eyebrow, title, and actions. |
| appBadge | App Store / Google Play download badge. |
| feature | Icon + title + description block for feature grids. |
| testimonial | Customer quote with avatar and star rating. |
| pricing | Plan card with feature list and CTA. Supports highlighted state. |
| accordion | Collapsible FAQ items — no JS, native <details>. |
| cta | Call-to-action block with eyebrow, heading, body, and actions slot. |
Layout
| Component | Description |
|---|---|
| container | Max-width wrapper with horizontal padding. |
| section | Vertical padding block with background variant. |
| grid | Responsive CSS grid. Collapses to one column on mobile. |
| stack | Flex column with consistent vertical gap. |
| cluster | Flex row with wrapping — for badges, buttons, etc. |
| divider | Horizontal rule, optionally with centred label. |
| banner | Full-width announcement bar above the nav. |
| media | Two-column image + text layout, stacks on mobile. |
| codeWindow | macOS window chrome around a code block. Accepts pre-highlighted HTML. |
| footer | Site footer with logo, nav links, and legal text. Stacks on mobile. |
Utility classes
pulse-ui.css ships a utility layer (prefix u-) for common spacing, typography, and layout needs. Reach for these before writing custom CSS — they use the same --ui-* tokens as components so theme overrides apply everywhere.
Spacing
Scale: 1=4px 2=8px 3=12px 4=16px 5=20px 6=24px 8=32px 10=40px 12=48px 16=64px
| Class | Property |
|---|---|
u-mt-{0–16} | margin-top |
u-mb-{0–16} | margin-bottom |
u-mx-auto | margin-left + right: auto |
u-p-{0–8} | padding (all sides) |
u-px-{0–8} | padding-left + right |
u-py-{0–8} | padding-top + bottom |
Typography
| Class | Effect |
|---|---|
u-text-{xs,sm,base,lg,xl,2xl,3xl,4xl} | Font size + matching line-height |
u-font-{normal,medium,semibold,bold} | Font weight |
u-text-{left,center,right} | Text alignment |
u-text-{default,muted,accent,green,red,yellow,blue} | Token colour |
u-leading-{tight,snug,normal,relaxed,loose} | Line height |
Layout
| Class | Effect |
|---|---|
u-flex / u-flex-col | Flex row or column |
u-items-{start,center,end,stretch} | align-items |
u-justify-{start,center,end,between} | justify-content |
u-gap-{1–8} | gap |
u-w-full | width: 100% |
u-max-w-{xs,sm,md,lg,xl,prose} | max-width (320px–1024px, 65ch) |
u-hidden / u-block / u-inline-block | display |
Visual
| Class | Effect |
|---|---|
u-rounded / u-rounded-md / u-rounded-lg / u-rounded-full | border-radius |
u-border / u-border-t / u-border-b | 1px solid --ui-border |
u-bg-surface / u-bg-surface2 / u-bg-accent | background token |
u-overflow-hidden / u-overflow-auto | overflow |
u-opacity-50 / u-opacity-75 | opacity |
Utilities compose naturally with components and with each other:
<!-- centred hero block — no custom CSS needed -->
<"tok-fn">class="tok-kw">div "tok-fn">class="u-flex u-flex-col u-items-center u-text-center u-py-16 u-gap-4">
<"tok-fn">class="tok-kw">h1 "tok-fn">class="u-text-4xl u-font-bold">Hello</"tok-fn">class="tok-kw">h1>
<"tok-fn">class="tok-kw">p "tok-fn">class="u-text-lg u-text-muted u-max-w-prose">Subtitle goes here.</"tok-fn">class="tok-kw">p>
${button({ label: 'Get started', href: '/start' })}
</"tok-fn">class="tok-kw">div>
style="" attributes. Reach for utility classes first, and only add to app.css when you need something utilities cannot provide — a unique animation, a custom grid, or a one-off component variant.Composing components
Components compose naturally — pass the output of one as the content or footer of another. Here's a stat dashboard card:
import { card, stat, button } from '@invisibleloop/pulse/ui'
card({
title: 'This week',
content: `
${stat({ label: 'Page views', value: '48,291', change: '+12%', trend: 'up' })}
${stat({ label: 'New users', value: '1,042', change: '+4%', trend: 'up' })}
${stat({ label: 'Bounced', value: '22%', change: '+1%', trend: 'down' })}
`,
footer: button({ label: 'View full report', href: '/analytics', variant: 'ghost', size: 'sm' }),
})
label, title, value, and error are escaped automatically. content, footer, rows, actions, icon, action, and logo are raw HTML slots — they pass through as-is, so any user-supplied data going into those slots should go through escHtml() first.