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

laminar-ui

v0.1.2

Published

A React library for rendering UIs from streaming LLM / backend responses

Readme

Laminar UI

Render streaming LLM output in React.

LLMs stream text as a live feed of raw chunks. Mid-stream that output is broken containing unclosed code fences, half-finished tables, dangling ** and "_" tokens. Laminar handles incomplete markdown and JSON streams correctly, so your UI never breaks while the response is still arriving.

It handles the full output layer: parsing the SSE stream, healing torn markdown on every frame, and rendering it cleanly while tokens are still arriving.

npm install laminar-ui

What it does

  • Parses SSE streams from any LLM provider out of the box
  • Patches incomplete markdown syntax in real time so rendering never breaks mid-stream
  • Streams and progressively renders partial JSON for structured LLM outputs
  • Works with any backend: raw fetch, Vercel AI SDK, Express, or your own setup
  • Fully themeable via CSS variables
  • Syntax highlighting in code blocks via Shiki, lazy-loaded so it doesn't impact your initial bundle

StreamMarkdown

A drop in component for streaming output in Markdown. Give it a fetcher that returns a streaming Response and it handles everything else.

import { StreamMarkdown } from "laminar-ui";

<StreamMarkdown
  fetcher={() =>
    fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({ message: input }),
      stream: true, // important to make the response streaming as laminar only handles streaming responses.
    })
  }
  onFinish={() => console.log("Stream complete")}
/>;

What it handles mid-stream

Laminar patches broken syntax on every render frame before passing it to the parser, so the output always looks correct even when the LLM is mid-sentence.

| Scenario | Example mid-stream | What Laminar does | | -------------------- | ------------------------- | --------------------------------------------- | | Unclosed code fence | ```python\nprint("hel | Auto-closes the block | | Incomplete table row | \| Name \| Age | Adds closing pipe + newline | | Dangling tokens | Some **bold | Strips for this frame, restores when complete |

Custom components

Override any rendered element with your own:

<StreamMarkdown
  fetcher={fetcher}
  components={{
    h1: ({ children }) => <h1 className="my-heading">{children}</h1>,
    code: ({ inline, lang, children }) =>
      inline ? (
        <code className="inline">{children}</code>
      ) : (
        <MyCodeBlock lang={lang}>{children}</MyCodeBlock>
      ),
    a: ({ href, children }) => (
      <a href={href} target="_blank" className="my-link">
        {children}
      </a>
    ),
  }}
/>

Available overrides: h1 h2 h3 p a code table


StreamText

A lightweight alternative to StreamMarkdown for plain text output. Use this when the LLM response doesn't contain markdown, only simple prose, single-line answers, or any output where markdown rendering adds unnecessary overhead.

import { StreamText } from "laminar-ui";

<StreamText
  fetcher={() =>
    fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({ message: input }),
      stream: true,
    })
  }
  onFinish={() => console.log("Stream complete")}
/>;

Renders the raw text stream as-is with no markdown parsing or syntax highlighting. If you need markdown, use StreamMarkdown instead.


useStream

The hook behind StreamMarkdown. Use this when you need direct control when building a custom renderer, triggering streams imperatively, or wiring into your own component tree.

import { useStream } from "laminar-ui";

function Chat() {
  const { text, status, error, start, stop } = useStream();

  return (
    <>
      <button
        onClick={() => start(() => fetch("/api/chat", { method: "POST" }))}
      >
        Send
      </button>
      <button onClick={stop} disabled={status !== "streaming"}>
        Stop
      </button>
      <pre>{text}</pre>
    </>
  );
}

status moves through: "idle""streaming""done" (or "error" / "cancelled")


useStreamingJSON

When your LLM returns structured JSON instead of prose. You get a live, partially-hydrated JavaScript object on every frame and fields appear as they arrive, before the stream is complete.

import { useStreamingJSON } from "laminar-ui";

type Recipe = {
  title: string;
  ingredients: string[];
  steps: string[];
};

const { data, status, start } = useStreamingJSON<Recipe>();

// Start streaming
start(() => fetch("/api/generate-recipe", { method: "POST" }));

// Render progressively; title appears before ingredients are done
<h1>{data?.title}</h1>
<ul>
  {data?.ingredients?.map((item, i) => <li key={i}>{item}</li>)}
</ul>

Uses partial-json internally to safely parse incomplete JSON on every frame without throwing.


createStreamAdapter

The SSE parser at Laminar's core. Reads a streaming Response, parses data: lines, and yields typed tokens. This function is framework-agnostic and works in any JavaScript environment.

import { createStreamAdapter } from "laminar-ui/core";

const response = await fetch("/api/chat", { method: "POST" });
const adapter = createStreamAdapter(response);

for await (const token of adapter) {
  if (token.type === "text") process(token.content);
  if (token.type === "done") finalize();
  if (token.type === "error") handleError(token.content);
}

Provider formats

Laminar defaults to OpenAI's SSE format. Pass a custom extractText function for other providers:

// OpenAI (default)
createStreamAdapter(response);

// Anthropic
createStreamAdapter(response, (json) => json.delta?.text);

// Gemini
createStreamAdapter(
  response,
  (json) => json.candidates?.[0]?.content?.parts?.[0]?.text,
);

// Your own backend
createStreamAdapter(response, (json) => json.output);

The same extractText option is available on StreamMarkdown, useStream, and useStreamingJSON.


Integrations

With Vercel AI SDK

Already using useChat for message state? Laminar just replaces the rendering part:

import { useChat } from "ai/react";
import { MarkdownRenderer } from "laminar-ui";

function Chat() {
  const { messages, input, handleSubmit, handleInputChange } = useChat();

  return (
    <>
      {messages.map((msg) => (
        <div key={msg.id}>
          {msg.role === "assistant" ? (
            <MarkdownRenderer text={msg.content} />
          ) : (
            <p>{msg.content}</p>
          )}
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </>
  );
}

With a custom backend

No SDK needed. Point fetcher at your own endpoint:

<StreamMarkdown
  fetcher={() =>
    fetch("https://your-api.com/chat", {
      method: "POST",
      headers: { Authorization: `Bearer ${token}` },
      body: JSON.stringify({ prompt: userMessage }),
    })
  }
  extractText={(json) => json.output} // match your API's shape
/>

Directly with OpenAI / Anthropic

// OpenAI
<StreamMarkdown
  fetcher={() =>
    fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${OPENAI_KEY}`,
      },
      body: JSON.stringify({
        model: "gpt-4o",
        stream: true,
        messages: [{ role: "user", content: prompt }],
      }),
    })
  }
/>

// Anthropic
<StreamMarkdown
  fetcher={() =>
    fetch("https://api.anthropic.com/v1/messages", { ... })
  }
  extractText={(json) => json.delta?.text}
/>

Theming

All visual styling is controlled via CSS variables. Override them globally to match your design system.

:root {
  --lm-heading: #111827;
  --lm-link: #2563eb;
  --lm-border: #e5e7eb;
  --lm-inline-code-bg: #f3f4f6;
  --lm-blockquote-border: #d1d5db;
  --lm-blockquote-text: #6b7280;
  --lm-del: #ef4444;
}

/* Dark mode */
[data-theme="dark"] {
  --lm-heading: #f9fafb;
  --lm-link: #60a5fa;
  --lm-border: #374151;
  --lm-inline-code-bg: #1f2937;
  --lm-blockquote-border: #4b5563;
  --lm-blockquote-text: #9ca3af;
  --lm-del: #f87171;
}

Import the base stylesheet once at your app root:

import "laminar-ui/laminar.css";

API Reference

<StreamMarkdown />

| Prop | Type | Required | Description | | ------------- | ------------------------- | -------- | ------------------------------------------------- | | fetcher | () => Promise<Response> | ✓ | Function that initiates the LLM stream | | onFinish | () => void | | Called when stream completes | | extractText | (json: any) => string | | Custom token extractor. Defaults to OpenAI format | | components | ComponentsMap | | Override rendered elements |

<StreamText />

| Prop | Type | Required | Description | | ------------- | ------------------------- | -------- | ------------------------------------------------- | | fetcher | () => Promise<Response> | ✓ | Function that initiates the LLM stream | | onFinish | () => void | | Called when stream completes | | extractText | (json: any) => string | | Custom token extractor. Defaults to OpenAI format |

useStream()

| | Type | Description | | -------- | --------------------------------- | ------------------------------------------------------------------- | | text | string | Full accumulated text so far | | status | StreamStatus | "idle" | "streaming" | "done" | "error" | "cancelled" | | error | string \| null | Error message if status is "error" | | start | (fetcher, extractText?) => void | Begin streaming | | stop | () => void | Cancel the stream |

useStreamingJSON<T>()

| | Type | Description | | -------- | ---------------------------------- | ---------------------------------------- | | data | T \| null | Live partial object, updated every frame | | status | StreamStatus | Same as useStream | | error | string \| null | Error message if status is "error" | | start | (response, extractText?) => void | Begin streaming | | stop | () => void | Cancel the stream |


Contributing

Issues and PRs are welcome.

git clone https://github.com/Yashkanekar/laminar-ui
cd laminar-ui
npm install
npm run dev

License

MIT