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

feather-render

v1.3.3

Published

A feather light render framework

Readme

Feather Render

gzip license version

✨ A feather light render framework ✨ 721 bytes minified and gzipped - no dependencies - SSR support

Companion framework:

Live examples:

coffee

Getting started

Package

npm i feather-render

...or inline

<head>
  <script src="feather-render.min.js"></script>
</head>
<body>
  <script>
    const { html, hydrate } = window.__feather__ || {};
  </script>
</body>

Index

Usage

Documentation

Definitions

Examples

Usage

Basic syntax

import { FR, html } from 'feather-render';
import { TodoItemProps } from './TodoItem.types';

const TodoItem: FR<TodoItemProps> = ({ todo }) => {
  return html`
    <li>${todo.title}</li>
  `;
};

const TodoList: FR = () => {
  return html`
    <ul>${todos.map(todo => TodoItem({ todo }))}</ul>
  `;
};

const Document: FR = () => html`
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Feather</title>
      <script type="module" src="index.mjs"></script>
    </head>
    <body>
      ${TodoList()}
    </body>
  </html>
`;

Tip: Plugins for VSCode like lit-html or Inline HTML can be used for syntax highlighting.

Server-Side Rendering (SSR)

import express from 'express';
import { Document } from './components/Document';

const server = express();

server.use(express.static('dist'));

server.get('/', (req, res) => {
  const document = Document({ req });
  res.send(document.toString());
});

server.listen(5000);

Client hydration

import { hydrate } from 'feather-render';
import { TodoList } from './components/TodoList.js';

hydrate(TodoList(), document.body);

Documentation

html()

const render = html`<div></div>`;

Parameters

  • string - html template string to render

Return value

html().mount()

mount(() => {
  console.log('Component inserted in DOM');
});

Parameters

  • callback() - function called when component is inserted in DOM

Return value

  • void

html().unmount()

unmount(() => {
  console.log('Component removed from DOM');
});

Parameters

  • callback() - function called after component is removed from DOM

Return value

  • void

hydrate()

hydrate(App(), document.body);

Parameters

  • element - Render from html()
  • target - where to mount the element

Return value

  • void

Definitions

Render

Return from html()

const { refs, render, element, mount, unmount } = html`<div></div>`;
  • refs - list of id'ed elements
  • render - this
  • element - element to insert in DOM
  • mount() - set callback for mount
  • unmount() - set callback for unmount

FR

Feather Render

const Page: FR<Props> = (props) => {
  return html`
    <main>
      ${props.title}
    </main>
  `;
};

FP

Feather Render Promise

const Page: FP<Props> = async (props) => {
  const pageData = await fetchPageData(props);

  return html`
    <main>
      ${pageData.title}
    </main>
  `;
};

Examples

Re-rendering

Primitive values

import { store } from 'feather-state';
import { FR, html } from 'feather-render';

const { watch, ...state } = store({
  greeting: 'Hello, World'
});

const Component: FR = () => {
  const { refs, render } = html`
    <p id="paragraph">${state.greeting}</p>
  `;

  // Watch greeting + update DOM
  watch(state, 'greeting', (next) => {
    refs.paragraph?.replaceChildren(next);
  });

  // Change greeting state
  setTimeout(() => {
    state.greeting = 'Hello, back!';
  }, 1000);

  return render;
};

Lists

import { store } from 'feather-state';
import { FR, html } from 'feather-render';
import { TodoItemProps } from './TodoItem.types';

const { watch, ...state  } = store({
  todos: ['Todo 1', 'Todo 2'];
});

const TodoItem: FR<TodoItemProps> = ({ todo }) => {
  return html`
    <li>${todo}</ul>
  `;
};

const TodoList: FR = () => {
  const { refs, render } = html`
    <ul id="todoList">
      ${state.todos.map(todo => (
        TodoItem({ todo })
      ))}
    </ul>
  `;

  const reRenderTodos = () => {
    const fragment = new DocumentFragment();
    for (let todo of todoStore.todos) {
      const { element } = TodoItem({ todo });
      element && fragment.appendChild(element);
    }
    refs.todoList?.replaceChildren(fragment);
  };

  // Watch todos + update DOM
  watch(state, 'todos', () => {
    reRenderTodos();
  });

  // Append todo in state
  setTimeout(() => {
    state.todos = [...state.todos, 'Todo 3'];
  }, 1000);

  return render;
};

Event listeners

Form submission

import { FR, html } from 'feather-render';

const Component: FR = () => {
  const { refs, render, mount, unmount } = html`
    <form id="form">
      <p id="status">Fill in form</p>
      <input type="text" />
      <button type="submit">Submit</button>
    </form>
  `;

  const handleSubmit = (event) => {
    event.preventDefault();
    refs.status?.replaceChildren('Submitting');
  };

  mount(() => {
    refs.form?.addEventListener('submit', handleSubmit);
  });
  unmount(() => {
    refs.form?.removeEventListener('submit', handleSubmit);
  });

  return render;
};

Async components

Server

import express from 'express';
import { Document } from './components/Document';

const server = express();

server.get('/', async (req, res) => {
  const document = await Document({ req });
  res.send(document.toString());
});

server.listen(5000);

Client hydration

import { hydrate } from 'feather-render';
import { fetchPage } from './Document.helpers';

fetchPage('/').then(page => {
  hydrate(page, document.body);
});

Document helper

import { Render } from 'feather-render';
import { ErrorPage } from './ErrorPage';
import { Page } from './Page';

export const fetchPage = async (path: string): Promise<Render> => {
  try {
    const pageData = await (await fetch(`/api/page${path}`)).json();
    return pageData ? Page({ pageData }) : ErrorPage({ code: 404 });
  } catch {
    return ErrorPage({ code: 500 });
  }
};

Document

import { Request } from 'express';
import { FP, html } from 'feather-render';
import { fetchPage } from './Document.helpers';

type Props = {
  req: Request;
};

const Document: FP<Props> = async ({ req }) => html`
  <!doctype html>
  <html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Feather</title>
    <script type="module" src="/index.mjs"></script>
  </head>
  <body>
    ${await fetchPage({ path: req.path })}
  </body>
  </html>
`;

Fetching

Server and client

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render } = html``;

  fetch('http://localhost:5000/api/v1/user')
    .then(res => res.json())
    .then(res => console.log(res));

  return render;
};

Server or client

import { FR, html } from 'feather-render';

const isServer = () => typeof window === 'undefined';
const isClient = () => typeof window !== 'undefined';

const App: FR = () => {
  const { render } = html``;

  if (isServer()) {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  }

  if (isClient()) {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  }

  return render;
};

On mount

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render, mount } = html``;

  mount(() => {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  });

  return render;
};

Lazy / Suspense

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render } = html`
    <div id="lazyParent"></div>
  `;

  import('./LazyComponent').then(({ LazyComponent }) => {
    const { element } = LazyComponent();
    element && refs.lazyParent?.replaceChildren(element);
  });

  return render;
};

Unique id's

let i = 0;
export function id(name: string) {
  return `${name}_${i++}`;
}
import { FR, html } from 'feather-render';
import { id } from '../helpers/id';

const App: FR = () => {
  const uniqueId = id('unique');

  const { refs, render, mount } = html`
    <div id=${uniqueId}></div>
  `;

  mount(() => {
    refs[uniqueId]?.replaceChildren('Component mounted');
  });

  return render;
};

CSS in JS

Components

import { FR, html } from 'feather-render';
import { css } from '@emotion/css';

const Page: FR = () => html`
  <main class=${css`background: red;`}>
  </main>
`;

Server-Side Rendering (SSR)

import { FR, html } from 'feather-render';
import createEmotionServer from '@emotion/server/create-instance';
import { cache } from '@emotion/css';
import { Page } from './Page';

const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

const Document: FR = () => {
  const page = Page();
  const chunks = extractCriticalToChunks(page.toString());
  const styles = constructStyleTagsFromChunks(chunks);

  return html`
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Feather</title>
      <script type="module" src="/index.mjs"></script>
      ${styles}
    </head>
    <body>
      ${page}
    </body>
    </html>
  `;
};

Roadmap 🚀

  • CLI tool
  • Cleaner way of referencing values in html
  • Binding values, re-renders and listeners
  • Router example