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

@useblok/core

v0.2.1

Published

Visual block editor for React. Docs: https://docs.useblok.dev · Demo: https://demo.useblok.dev

Downloads

464

Readme

@useblok/core

Modern visual block editor for React. Drag-and-drop canvas, auto-generated forms from field definitions, slots, undo/redo, keyboard shortcuts, and a layered panel UI — with plugins, comments, versioning, audit, permissions, and realtime.

→ Try the live demo

┌ Top bar  ─────────────────────────────────────────────────────────┐
│ Title · History · Undo/Redo · Save · Publish                      │
├─┬──────────┬───────────────────────┬────────────┬─────────────────┤
│R│ Layers / │                       │  Form /    │  Edit · Info    │
│a│ Blocks   │    Live canvas        │  Settings  │  Tools · Config │
│i│          │    (device frame)     │            │                 │
│l└──────────┴───────────────────────┴────────────┴─────────────────┘

Install

npm install @useblok/core
# or
pnpm add @useblok/core
# or
yarn add @useblok/core

Peer deps: react ^18.2 || ^19 and react-dom ^18.2 || ^19.

Quickstart

// app/layout.tsx (once, globally)
import "@useblok/core/styles.css";
// app/editor/page.tsx
"use client";

import { Blok, type Config, type Data } from "@useblok/core";

const config: Config = {
  components: {
    Hero: {
      label: "Hero",
      fields: {
        title: { type: "text", label: "Title", required: true },
        subtitle: { type: "textarea", label: "Subtitle", rows: 3 },
      },
      defaultProps: { title: "Hello", subtitle: "" },
      render: ({ title, subtitle }) => (
        <section>
          <h1>{title}</h1>
          <p>{subtitle}</p>
        </section>
      ),
    },
  },
};

export default function Page() {
  return (
    <Blok
      config={config}
      onSave={(data: Data) => fetch("/api/save", { method: "POST", body: JSON.stringify(data) })}
    />
  );
}

The bundle ships with "use client", so it works from any Next.js Server Component import path too — you don't have to mark the importer yourself (though Blok itself must be rendered inside a client boundary, same as any interactive component).

Concepts

Components

A component is a user-defined block type. Declare its fields (which become the auto-form), a default shape, and a React renderer:

Hero: {
  label: "Hero",
  description: "Big banner with a headline and CTA.",
  folder: "Marketing",         // groups in the Blocks palette
  accent: "#13a58f",           // icon tint
  fields: { /* ... */ },
  defaultProps: { /* ... */ },
  render: (props) => <section>{...}</section>,
  getSummary: (props) => props.title as string,  // shown in Layers/Edit rows
}

Fields

The builtin field types:

| type | Renders | Value | | --- | --- | --- | | text | single-line input | string | | textarea | multi-line | string | | richtext | toolbar + textarea (markdown-flavoured) | string | | number | number input (min/max/step) | number | | boolean | toggle switch | boolean | | select | dropdown | unknown | | radio | segmented pill | unknown | | link | URL + label + target | { url, label, target } | | asset | URL + preview + alt | { url, alt } | | array | collapsible list of sub-forms | Record<string, unknown>[] | | object | inline nested form | Record<string, unknown> | | slot | canvas drop zone | blocks stored in data.zones | | custom | whatever you render | anything |

Every field supports label, description, required, icon. Required fields show a red * in the form.

Slots

A slot lets a block contain other blocks — think <Container> wrapping a stack of sections. Declare a slot field, then use the blok.renderSlot helper in your render:

Container: {
  fields: {
    background: { type: "select", options: [...] },
    children: { type: "slot", label: "Contents" },
  },
  defaultProps: { background: "#fff" },
  render: ({ background, blok }) => (
    <div style={{ background, padding: 24 }}>
      {blok.renderSlot("children")}
    </div>
  ),
}

Slot blocks are stored in data.zones[${blockId}:${fieldName}]. Use slotZoneId(blockId, fieldName) to compute the key.

Root

Wrap the whole document in a root component (an HTML scaffold, theme provider, layout grid, whatever):

root: {
  fields: { description: { type: "textarea", label: "SEO description" } },
  defaultProps: { title: "Untitled" },
  render: ({ children }) => <main>{children}</main>,
}

Categories

Organise the Blocks palette beyond single-folder grouping:

categories: {
  marketing: { title: "Marketing", description: "Hero, CTAs, grids." },
  layout:    { title: "Layout", defaultExpanded: false },
},

Props

<Blok
  config={config}
  data={initialData}                 // optional; defaults to empty doc
  title="Untitled"                   // shown in TopBar
  slug="docs / new"                  // shown under title
  previewUrl="/preview"              // link-to-preview icon
  historyLimit={100}                 // undo ring buffer size
  onSave={(data) => { /* ... */ }}   // ⌘S, Save button, Publish
  onPublish={(data) => { /* ... */ }}
/>

onSave and onPublish receive the current document from the store — safe to read in async handlers.

Keyboard shortcuts

| Keys | Action | | --- | --- | | ⌘/Ctrl + S | Save | | ⌘/Ctrl + Z · ⌘⇧Z | Undo · Redo | | ⌘/Ctrl + D | Duplicate selected block | | / | Select sibling block | | ⌥ Alt + / | Move selected block | | Delete · Backspace | Remove selected | | Esc | Clear selection |

Hooks (for custom UI)

Everything the builtin UI does is driven by hooks you can re-use:

useData()          // current document
useConfig()        // full Config
useSelection()     // selected block id (or null)
useEditing()       // the block whose Edit form is open
useHistory()       // { canUndo, canRedo }
useDirty()         // true if changes since last save
useViewport()      // "desktop" | "tablet" | "mobile" | "fullscreen"
useDrillPath()     // drill tokens when focused into an array/slot
useActions()       // { addBlock, moveBlock, duplicateBlock, undo, redo, ... }
useKeyboardShortcuts({ onSave })  // register the default shortcut set

Every hook must be called inside a <Blok> or a <BlokStoreProvider>.

Headless mode

Drive the editor yourself with BlokStoreProvider:

import { BlokStoreProvider, useData, useActions } from "@useblok/core";

<BlokStoreProvider config={config} data={initial}>
  <MyOwnCanvas />
  <MyOwnPanel />
</BlokStoreProvider>

Data shape

interface Data {
  root: { props?: Record<string, unknown> };
  content: BlockInstance[];                    // top-level blocks
  zones?: Record<string, BlockInstance[]>;     // keyed by `${blockId}:${slotFieldName}`
}

interface BlockInstance {
  type: string;                 // key in config.components
  props: { id: string } & Record<string, unknown>;
}

The shape is JSON-serialisable by design: save it, ship it between server and client, version it.

License

MIT.