GitHub

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

ComponentDescription
buttonRenders as <button> or <a>. Four variants, three sizes.
badgeInline status label. Five semantic colour variants.
cardContent surface with title, body, and optional footer.
inputLabelled text input with hint and error support.
fieldsetSemantic grouping of related fields with an accessible legend.
selectStyled select with option groups and current-value support.
textareaMulti-line input with label, hint, and error.
alertInline feedback banner. ARIA roles wired by variant.
statNumeric metric with optional trend arrow.
avatarUser avatar — image with fallback to initials.
emptyEmpty state with title, description, and optional CTA.
tableAccessible data table with scroll wrapper.

Landing page

ComponentDescription
navSite header with logo, links, and optional CTA.
heroFull-width hero section with eyebrow, title, and actions.
appBadgeApp Store / Google Play download badge.
featureIcon + title + description block for feature grids.
testimonialCustomer quote with avatar and star rating.
pricingPlan card with feature list and CTA. Supports highlighted state.
accordionCollapsible FAQ items — no JS, native <details>.
ctaCall-to-action block with eyebrow, heading, body, and actions slot.

Layout

ComponentDescription
containerMax-width wrapper with horizontal padding.
sectionVertical padding block with background variant.
gridResponsive CSS grid. Collapses to one column on mobile.
stackFlex column with consistent vertical gap.
clusterFlex row with wrapping — for badges, buttons, etc.
dividerHorizontal rule, optionally with centred label.
bannerFull-width announcement bar above the nav.
mediaTwo-column image + text layout, stacks on mobile.
codeWindowmacOS window chrome around a code block. Accepts pre-highlighted HTML.
footerSite 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

ClassProperty
u-mt-{0–16}margin-top
u-mb-{0–16}margin-bottom
u-mx-automargin-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

ClassEffect
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

ClassEffect
u-flex / u-flex-colFlex 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-fullwidth: 100%
u-max-w-{xs,sm,md,lg,xl,prose}max-width (320px–1024px, 65ch)
u-hidden / u-block / u-inline-blockdisplay

Visual

ClassEffect
u-rounded / u-rounded-md / u-rounded-lg / u-rounded-fullborder-radius
u-border / u-border-t / u-border-b1px solid --ui-border
u-bg-surface / u-bg-surface2 / u-bg-accentbackground token
u-overflow-hidden / u-overflow-autooverflow
u-opacity-50 / u-opacity-75opacity

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>
Never use inline 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' }),
})
Text props like 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.