npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@onivoro/server-html

v24.38.2

Published

Server-side HTML generation with a JSX-like props interface — without JSX. No compilation step, no transpiler config, no `.tsx` files, no virtual DOM. Just functions that take props and return HTML strings.

Readme

@onivoro/server-html

Server-side HTML generation with a JSX-like props interface — without JSX. No compilation step, no transpiler config, no .tsx files, no virtual DOM. Just functions that take props and return HTML strings.

This matters when you need to generate HTML from a NestJS controller, a CLI tool, an email service, or any server context where introducing a JSX toolchain would be overkill. The API feels like writing React components, but every call is a pure function that returns a string — composable, testable, and zero-dependency.

All 112 HTML5 elements are covered with proper attribute escaping and typed CSSProperties support.

Installation

npm install @onivoro/server-html

Usage

Every HTML5 tag has a $-prefixed factory function that accepts a props object and returns an HTML string.

import { $div, $h1, $p, $button } from '@onivoro/server-html';

const html = $div({
  className: 'card',
  style: { padding: '1rem', border: '1px solid #ccc' },
  children: [
    $h1({ textContent: 'Hello' }),
    $p({ textContent: 'Server-rendered HTML.' }),
    $button({ '@click': 'handleClick()', textContent: 'Click me' })
  ]
});

Props

Every element factory accepts TElementProps:

| Prop | Type | Description | |------|------|-------------| | className | string | CSS class(es) | | style | CSSProperties | Inline styles (camelCase or kebab-case) | | children | Array<string \| number> | Nested content (other elements, text) | | textContent | string | Single text node | | innerHTML | string | Raw HTML content | | [key: string] | any | Any other HTML attribute |

Content precedence: innerHTML > textContent > children.

Self-Closing Elements

Self-closing tags (img, input, meta, br, hr, link, source, area, base, col, embed, track, wbr) render correctly:

import { $img, $input, $meta } from '@onivoro/server-html';

$img({ src: '/logo.png', alt: 'Logo', style: { maxWidth: '200px' } })
// <img src="/logo.png" alt="Logo" style="max-width: 200px;"/>

$input({ type: 'text', placeholder: 'Search...', className: 'search-input' })
// <input type="text" placeholder="Search..." class="search-input"/>

$meta({ charset: 'UTF-8' })
// <meta charset="UTF-8"/>

Components

Since every element factory is just a function that returns a string, building reusable components works exactly like React — define a function that accepts props and returns markup. No framework, no class hierarchy, no registration step.

Defining Components

import { $div, $h2, $p, $img, TElementProps, CSSProperties } from '@onivoro/server-html';

// A component is just a function — same mental model as a React functional component
interface CardProps {
  title: string;
  body: string;
  imageUrl?: string;
}

function Card({ title, body, imageUrl }: CardProps): string {
  return $div({
    className: 'card',
    style: cardStyle,
    children: [
      imageUrl ? $img({ src: imageUrl, alt: title, style: { width: '100%' } }) : '',
      $h2({ textContent: title }),
      $p({ textContent: body }),
    ]
  });
}

const cardStyle: CSSProperties = {
  border: '1px solid #e0e0e0',
  borderRadius: '8px',
  padding: '1rem',
  maxWidth: '320px',
};

Composing Components

Components compose exactly like React — call them inside children arrays:

import { $div, $h1 } from '@onivoro/server-html';

function CardGrid(cards: CardProps[]): string {
  return $div({
    children: [
      $h1({ textContent: 'Featured' }),
      $div({
        style: { display: 'flex', gap: '1rem', flexWrap: 'wrap' },
        children: cards.map(card => Card(card)),
      }),
    ]
  });
}

// Usage
const html = CardGrid([
  { title: 'First', body: 'Hello world' },
  { title: 'Second', body: 'Another card', imageUrl: '/img/photo.jpg' },
]);

Components with Children

Accept children as a prop to create layout components — the same pattern as props.children in React:

import { $div, $header, $main, $footer, $h1, CSSProperties } from '@onivoro/server-html';

function PageLayout({ title, children }: { title: string; children: string[] }): string {
  return $div({
    children: [
      $header({ children: [$h1({ textContent: title })] }),
      $main({ style: mainStyle, children }),
      $footer({ textContent: `© ${new Date().getFullYear()}` }),
    ]
  });
}

const mainStyle: CSSProperties = { padding: '2rem', maxWidth: '960px', margin: '0 auto' };

Styles and Scripts as Variables

Styles and scripts are just strings and objects — assign them to variables, share them across components, compose them. No special API needed.

Shared Styles

import { CSSProperties } from '@onivoro/server-html';

// Define once, reference everywhere — like a theme object in React
const theme = {
  primary: { backgroundColor: '#1976d2', color: '#fff' } as CSSProperties,
  surface: { backgroundColor: '#fff', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' } as CSSProperties,
  text: { fontFamily: 'system-ui, sans-serif', lineHeight: '1.6' } as CSSProperties,
};

// Compose styles with spread — like sx prop merging in MUI
const heroStyle: CSSProperties = { ...theme.primary, padding: '4rem 2rem', textAlign: 'center' };

Inline Scripts

import { $script, $style } from '@onivoro/server-html';

// Scripts are just strings — define them as constants alongside the components that use them
const analyticsScript = `
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'GA_MEASUREMENT_ID');
`;

const globalStyles = `
  * { box-sizing: border-box; margin: 0; }
  body { font-family: system-ui, sans-serif; }
  .container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
`;

// Then inject them into the page
$script({ textContent: analyticsScript });
$style({ textContent: globalStyles });

Putting It Together

import { $html, $head, $meta, $title, $style, $script, $body, $div, $h1 } from '@onivoro/server-html';

function renderPage(pageTitle: string, content: string[]): string {
  return $html({
    lang: 'en',
    children: [
      $head({
        children: [
          $meta({ charset: 'UTF-8' }),
          $meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }),
          $title({ textContent: pageTitle }),
          $style({ textContent: globalStyles }),
        ]
      }),
      $body({
        children: [
          $div({ className: 'container', children: content }),
          $script({ textContent: analyticsScript }),
        ]
      })
    ]
  });
}

Dynamic Content

import { $ul, $li, $table, $thead, $tbody, $tr, $th, $td } from '@onivoro/server-html';

// Lists — same as items.map() in React JSX
const items = ['Apple', 'Banana', 'Orange'];
const list = $ul({
  children: items.map(textContent => $li({ textContent }))
});

// Tables from data
interface User { name: string; email: string; role: string }
function UserTable(users: User[]): string {
  return $table({
    children: [
      $thead({ children: [$tr({ children: [
        $th({ textContent: 'Name' }),
        $th({ textContent: 'Email' }),
        $th({ textContent: 'Role' }),
      ]})] }),
      $tbody({ children: users.map(u => $tr({ children: [
        $td({ textContent: u.name }),
        $td({ textContent: u.email }),
        $td({ textContent: u.role }),
      ]})) }),
    ]
  });
}

Server-Rendered Pages with Client Interactivity

The props interface accepts arbitrary attributes, making it natural to pair with lightweight client frameworks like Alpine.js. Server-render the structure, let the client handle behavior:

import { $div, $button, $input, $ul } from '@onivoro/server-html';

const searchPage = $div({
  'x-data': '{ query: "", results: [] }',
  children: [
    $input({
      type: 'text',
      'x-model': 'query',
      '@input.debounce.300ms': 'search()',
      placeholder: 'Search...'
    }),
    $ul({
      'x-html': 'resultsHtml'
    }),
    $button({
      '@click': 'clearResults()',
      'x-show': 'results.length > 0',
      textContent: 'Clear'
    })
  ]
});

No template literals, no string concatenation, no forgetting to escape an attribute value. The same pattern works with HTMX, Stimulus, or any attribute-driven framework.

Custom Element Factories

Use asElementFactory to create factories for custom elements or web components:

import { asElementFactory } from '@onivoro/server-html';

// Provide a renderer function: (content: Array<string|number>, attrs: TAttributes) => string
// The easiest approach is to build on an existing element factory
import { $div } from '@onivoro/server-html';

// Semantic wrapper — same as aliasing a styled component in React
const $card = (props: { title: string; children?: string[] }) =>
  $div({
    className: 'card',
    children: [
      $div({ className: 'card-title', textContent: props.title }),
      ...(props.children || []),
    ]
  });

$card({ title: 'Hello', children: ['<p>Content</p>'] });

Available Elements

All HTML5 tags are available as $-prefixed exports:

$a, $abbr, $address, $area, $article, $aside, $audio, $b, $base, $bdi, $bdo, $blockquote, $body, $br, $button, $canvas, $caption, $cite, $code, $col, $colgroup, $data, $datalist, $dd, $del, $details, $dfn, $dialog, $div, $dl, $dt, $em, $embed, $fieldset, $figcaption, $figure, $footer, $form, $h1$h6, $head, $header, $hgroup, $hr, $html, $i, $iframe, $img, $input, $ins, $kbd, $label, $legend, $li, $link, $main, $map, $mark, $math, $menu, $meta, $meter, $nav, $noscript, $object, $ol, $optgroup, $option, $output, $p, $picture, $pre, $progress, $q, $rp, $rt, $ruby, $s, $samp, $script, $section, $select, $slot, $small, $source, $span, $strong, $style, $sub, $summary, $sup, $table, $tbody, $td, $template, $textarea, $tfoot, $th, $thead, $time, $title, $tr, $track, $u, $ul, $var, $video, $wbr

Also exported: CSSProperties, TElementProps, asElementFactory.

License

MIT