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

@codesuma/baseline

v1.0.15

Published

A minimal, imperative UI framework for building fast web apps. No virtual DOM, no magic, no dependencies.

Readme

Baseline

A minimal, imperative UI framework for building fast web apps. No virtual DOM, no magic, no dependencies.

~1000 lines of TypeScript that gives you everything you need for SPAs.

Installation

npm install @codesuma/baseline

Why Baseline?

  • Predictable - What you write is what happens. No hidden lifecycle, no reactivity magic.
  • Fast - Direct DOM manipulation. No diffing algorithms.
  • Tiny - The entire framework is smaller than most framework's "hello world" bundle.
  • Stable - No dependencies means no breaking changes from upstream.

Quick Start

import { Div, Button, router } from '@codesuma/baseline'

const App = () => {
    const app = Div()
    const count = { value: 0 }
    
    const counter = Div('0')
    counter.style({ fontSize: '48px', textAlign: 'center' })
    
    const btn = Button('Click me')
    btn.on('click', () => {
        count.value++
        counter.text(String(count.value))
    })
    
    app.style({ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' })
    app.append(counter, btn)
    
    return app
}

document.body.appendChild(App().el)

Core Concepts

Components

Every component is created with the Base() factory:

import { Base } from '@codesuma/baseline'

const card = Base('div')   // Creates a <div>
card.el                     // The actual DOM element
card.id                     // Unique identifier

Or use pre-made native components:

import { Div, Button, Input, Span, A, Img } from '@codesuma/baseline'

const container = Div('Hello')
const btn = Button('Click')
const input = Input('Enter name...', 'text')

// Support for different input types (First arg is label for check/radio)
const checkbox = Input('Stay signed in', 'checkbox', { checked: true })
const number = Input('Age', 'number', { min: 0, max: 100 })
const date = Input('Date', 'date')

// Checkbox helpers
checkbox.isChecked() // true
checkbox.toggle()
checkbox.on('change', (checked) => console.log(checked))

Events

Components have a built-in event emitter:

const btn = Button()

// Listen to events
btn.on('click', () => console.log('clicked!'))
btn.on('mounted', () => console.log('in the DOM'))

// Listen once
btn.once('click', () => console.log('first click only'))

// Emit custom events
btn.emit('my-event', { data: 123 })

// Remove listener
btn.off('click', handler)

Styling

Two approaches: inline styles or CSS Modules.

Inline Styles

Use for dynamic values and simple components:

// Inline styles
card.style({ 
    opacity: '0.5',
    transform: 'translateY(-10px)',
    transition: 'all 0.3s ease',
})

// Chained styles
card.style({ opacity: '0' })
    .style({ opacity: '1' })

CSS Modules (Recommended)

Use .module.css files for scoped, maintainable styles:

card/index.module.css

.base {
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    padding: 16px;
    transition: all 0.3s ease;
}

.base:hover {
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
    transform: translateY(-4px);
}

.dark {
    background: #1a1a1a;
    color: #fff;
}

card/index.ts

import { Div } from '@codesuma/baseline'
import styles from './index.module.css'

export const Card = () => {
    const base = Div()
    base.addClass(styles.base)
    
    // Toggle classes
    base.addClass(styles.dark)     // Add dark mode
    base.removeClass(styles.dark)  // Remove dark mode
    base.toggleClass(styles.dark, isDark)  // Conditional
    
    return base
}

Project Structure with CSS Modules

components/
├── card/
│   ├── index.ts           # Component logic
│   └── index.module.css   # Scoped styles
├── button/
│   ├── index.ts
│   └── index.module.css
pages/
├── home/
│   ├── index.ts
│   └── index.module.css

Rollup Configuration

Ensure your rollup.config.js includes postcss:

import postcss from 'rollup-plugin-postcss'

export default {
    plugins: [
        postcss({
            modules: true,        // Enable CSS modules
            extract: 'bundle.css', // Output file
            minimize: true,
        }),
    ],
}

Link the CSS in your HTML:

<link rel="stylesheet" href="/bundle.css">

DOM Tree

const list = Div()
const item1 = Div('First')
const item2 = Div('Second')

// Append children
list.append(item1, item2)

// Prepend
list.prepend(Div('Zero'))

// Conditional rendering
list.append(
    isLoggedIn && UserProfile(),
    showFooter ? Footer() : null
)

// Remove
item1.remove()

// Empty all children
list.empty()

// Access children
list.getChildren()

Lifecycle

const card = Div()

card.on('mounted', () => {
    // Called when component enters the DOM
    console.log('Card is visible')
})

card.on('unmounted', () => {
    // Called when component is removed
    console.log('Card removed')
})

Router

Base includes a full-featured SPA router with page lifecycle management. Pages stay in DOM and visibility toggles for smooth transitions.

Basic Setup

import { Div, router } from '@codesuma/baseline'
import { HomePage } from './pages/home'
import { AboutPage } from './pages/about'
import { UserPage } from './pages/user'

const view = Div()  // Container for pages
const app = Div()
app.append(view)

// Configure routes - pages stay in DOM, visibility toggles
router.routes({
    '/': HomePage,
    '/about': AboutPage,
    '/users/:id': UserPage,
}, view)

document.body.appendChild(app.el)

Route Configuration

router.routes({
    // Simple routes
    '/': HomePage,
    '/about': AboutPage,
    
    // Dynamic parameters
    '/users/:id': UserPage,
    '/posts/:postId/comments/:commentId': CommentPage,
}, view)

How It Works

  • All pages are created once and stay in DOM
  • Navigation toggles visibility via CSS classes
  • Smooth transitions because both pages exist during animation
  • Smart Directional Animations: Automatically slides UP for forward navigation and DOWN for back navigation
  • State preserved (scroll position, form inputs)

Page Component

Base includes a Page component with built-in CSS transitions for smooth page navigation:

import { Page, IRouteEvent } from '@codesuma/baseline'

export const AboutPage = () => {
    const page = Page()  // Includes directional enter/exit animations
    
    // Add your content
    page.append(header, body)
    
    // Default animations:
    // Forward: Enter Up / Exit Up
    // Back:    Enter Down / Exit Down
    return page
}

Default animations:

  • Forward: Slides up from bottom
  • Back: Slides down to bottom

Customizing Page Transitions

import { Page, IRouteEvent } from '@codesuma/baseline'

export const HomePage = () => {
    const page = Page()
    
    // Override default enter - show instantly
    page.off('enter')
    page.on('enter', async ({ from }: IRouteEvent) => {
        page.showInstant() // Assuming you implemented this helper or use style overrides
        // Load data...
    })
    
    return page
}

Page methods:

  • showInstant() - Show page without animation
  • exitDown() - Exit with downward slide (for back navigation)

Page Lifecycle Events

Pages receive enter and exit events from the router:

page.on('enter', async ({ params, query, from, data }: IRouteEvent) => {
    // params: { id: '123' } for /users/:id
    // query: { sort: 'name' } for ?sort=name
    // from: '/home' - previous path
    // data: custom data from router.goto()
})

page.on('exit', async () => {
    // Called when navigating away
    // Async supported - router waits for completion
})

Navigation

import { router } from '@codesuma/baseline'

// Navigate to path
router.goto('/about')

// With query params
router.goto('/search?q=hello&page=2')

// With custom data (available in enter event)
router.goto('/users/123', { fromNotification: true })

// Browser history
router.back()
router.forward()

// Get current state
router.getPath()               // '/users/123'
router.getParams()             // { id: '123' }
router.getQuery()              // { sort: 'name' }
router.getQuery('sort')        // 'name'

// For heavy pages - manually destroy to force recreation
router.destroyPage('/heavy-page')

Resetting Heavy Pages

Since pages stay in DOM, you may want to reset state on exit for heavy pages:

page.on('exit', () => {
    // Reset scroll position
    page.el.scrollTop = 0
    
    // Clear dynamic content
    list.empty()
    
    // Reset form inputs
    input.val('')
})

// Or completely destroy the page to force recreation on next visit:
page.on('exit', () => {
    router.destroyPage('/heavy-page')
})

### Listening to Route Changes

For global navigation handling:

```typescript
router.on('change', ({ path, params, query, from, data }) => {
    // Update navigation UI
    updateActiveMenuItem(path)
    
    // Analytics
    trackPageView(path)
    
    // Show/hide back button
    backButton.style({ display: from ? 'block' : 'none' })
})

Complete Example

// app.ts
import { Div, router } from '@codesuma/baseline'
import { FIXED, EASE } from '@codesuma/baseline/helpers/style' // specialized helpers might still need specific paths if not all exported, but index.ts didn't show helpers/style exported. Let's check index.ts again.
import { HomePage } from './pages/home'
import { AboutPage } from './pages/about'
import { UserPage } from './pages/user'

const view = Div()
const app = Div()
const nav = Div()

// Navigation
const homeLink = Div('Home')
homeLink.on('click', () => router.goto('/'))

const aboutLink = Div('About')
aboutLink.on('click', () => router.goto('/about'))

nav.style({ display: 'flex', gap: '20px', padding: '10px' })
nav.append(homeLink, aboutLink)

app.style({ ...FIXED })
app.append(nav, view)

// Configure routes
router.routes({
    '/': { page: HomePage, cache: true },
    '/about': { page: AboutPage, cache: false },
    '/users/:id': { page: UserPage, cache: true },
}, view)

export default app
// pages/user/index.ts
import { Div, http } from '@codesuma/baseline'

export const UserPage = () => {
    const page = Div()
    const name = Div()
    const email = Div()
    
    page.append(name, email)
    
    page.on('enter', async ({ params }) => {
        const { data } = await http.get(`/api/users/${params.id}`)
        name.text(data.name)
        email.text(data.email)
    })
    
    return page
}

HTTP Client

Built-in fetch wrapper with request deduplication:

import { http } from '@codesuma/baseline'

// GET (automatically deduplicated - same URL = same request)
const { status, data } = await http.get('/api/users')

// POST, PUT, PATCH, DELETE
await http.post('/api/users', { name: 'John' })
await http.put('/api/users/1', { name: 'Jane' })
await http.patch('/api/users/1', { active: true })
await http.delete('/api/users/1')

// With auth
await http.get('/api/me', { auth: 'my-token' })

// Upload with progress
await http.upload('/api/upload', file, {
    onProgress: (loaded, total) => {
        console.log(`${Math.round(loaded/total * 100)}%`)
    }
})

Storage

localStorage

import { ldb } from '@codesuma/baseline'

ldb.set('user', { name: 'John' })  // Auto JSON stringify
ldb.get('user')                     // Auto JSON parse
ldb.remove('user')
ldb.clear()

IndexedDB

import { idb } from '@codesuma/baseline'

const db = idb('my-app')

// Create store (run once on app init)
await db.createStore('users', 1, { keyPath: 'id', indices: ['email'] })

// CRUD
await db.save('users', { id: '1', name: 'John', email: '[email protected]' })
const user = await db.get('users', '1')
const allUsers = await db.all('users')
await db.update('users', { id: '1', name: 'Jane' })
await db.delete('users', '1')

// Query
const results = await db.find('users', { 
    index: 'email', 
    value: '[email protected]',
    limit: 10,
    reverse: true
})

Global State

import { state } from '@codesuma/baseline'

// Simple get/set
state.set('user', { name: 'John', id: 123 })
state.get('user')  // { name: 'John', id: 123 }

// Subscribe to changes
state.on('user', (user) => {
    console.log('User changed:', user)
})

// Subscribe to any change
state.on('change', ({ key, value }) => {
    console.log(`${key} changed`)
})

Helpers

Style Helpers

import { ABSOLUTE, FIXED, EASE, WH, Y, CENTER } from '@codesuma/baseline/helpers/style'

card.style({ ...ABSOLUTE })           // position: absolute; inset: 0;
card.style({ ...CENTER })             // display: flex; align-items: center; justify-content: center;
card.style({ ...EASE(0.3) })          // transition: all 0.3s ease;
card.style({ ...WH(100, 50) })        // width: 100px; height: 50px;
card.style({ ...Y(-10) })             // transform: translateY(-10px);

Ripple Effect

Enable the ripple effect globally in your main entry file:

import { initRipple, Button } from '@codesuma/baseline'

// Initialize once
initRipple()

// Usage
const btn = Button('Click me')
btn.attr('data-ripple', '') // Add ripple attribute

Device Detection

import { isMobile, isTouch } from '@codesuma/baseline/helpers/device'

if (isMobile()) { /* mobile layout */ }
if (isTouch()) { /* touch interactions */ }

Validation

import { isEmail } from '@codesuma/baseline/helpers/regex'

if (isEmail(input.value())) { /* valid */ }

Project Structure

my-app/
├── base/                 # The framework (copy or npm install)
├── components/           # Your reusable components
│   └── card/
│   └── index.module.css  # CSS module
│   └── index.ts          # Component
├── pages/                # Page components
│   ├── home/
│   │   └── index.ts
│   └── about/
│       └── index.ts
├── services/             # API, state management
├── styles/               # CSS files
├── app.ts                # App setup with router
├── index.ts              # Entry point
└── index.html

Philosophy

  1. Imperative over declarative - You control the DOM directly
  2. Explicit over implicit - No hidden state updates or re-renders
  3. Composition over inheritance - Mix capabilities with Object.assign
  4. Simplicity over features - Small API surface, easy to learn

MIT License