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

@oliveryasuna/ink-frame

v1.1.0

Published

Readme

Banner

Check out my awesome-ink list for more awe-mazing-tastic Ink libraries!

ink-frame

npm

Grids of bordered boxes for Ink, where the borders actually join where they meet.

Ink's own box borders are fine for a single box. Put two of them next to each other and the seam between them comes out as ││, two parallel lines instead of one shared edge. That's because a box border is one unbroken line and there's nowhere to hang a or a part-way along it. ink-frame sidesteps that by painting every border into a single character grid and resolving each cell once, so a spot where four boxes meet becomes a and a T-junction becomes a , , and so on, without you ever writing those characters yourself.

Example

Install

bun add @oliveryasuna/ink-frame

# or pnpm
pnpm add @oliveryasuna/ink-frame

# or npm
npm install @oliveryasuna/ink-frame

# or yarn
yarn add @oliveryasuna/ink-frame

It expects ink (>=5) and react (>=18) alongside it, which you'll already have if you're building an Ink app.

The idea

You describe your layout as rectangles, hand those rectangles to Frame, and it works out the borders. A rectangle is just outer bounds in terminal cells:

interface Rect {
  left: number;
  top: number;
  width: number;
  height: number;
}

You rarely build those by hand. splitColumns and splitRows carve one rect into several, and they overlap adjacent rects by a single cell on purpose. That shared cell is the whole trick: both boxes paint an edge into it, so it resolves to a junction rather than to two lines sitting side by side.

import {render, Text, useStdout} from 'ink';
import {Frame, Pane, splitColumns, splitRows} from '@oliveryasuna/ink-frame';

const App = () => {
  const {stdout} = useStdout();
  const width = stdout.columns || 80;
  const height = stdout.rows || 24;

  const root = {left: 0, top: 0, width, height};

  // '3' is three cells tall (two borders plus a line of content);
  // 'grow' takes whatever the fixed bands leave behind.
  const [header, body, footer] = splitRows(root, [3, 'grow', 3]);
  const [side, main] = splitColumns(body, [24, 'grow']);

  return (
    <Frame width={width} height={height} borderColor="cyan">
      <Pane rect={header} paddingX={1}><Text bold>My App</Text></Pane>
      <Pane rect={side} paddingX={1}><Text>sidebar</Text></Pane>
      <Pane rect={main} paddingX={1}><Text>main</Text></Pane>
      <Pane rect={footer} paddingX={1}><Text dimColor>status line</Text></Pane>
    </Frame>
  );
};

render(<App />);

Splits nest as deep as you want. Split the body into columns, split one of those columns into rows, keep going. And a rect doesn't have to come from a split at all. Build one by hand inside another pane and its border stays separate rather than merging with the pane around it, which is how you get a framed box floating inside a larger region.

Panes

Pane doesn't render anything on its own. Frame reads the rect off each Pane during its own layout, draws the border, and then lays your content inside that border with absolute positioning. So your pane's children are ordinary Ink elements that know nothing about borders or where they sit on screen.

Every Box prop passes straight through to the content area, so paddingX, flexDirection, justifyContent, and the rest work as you'd expect:

<Pane rect={header} paddingX={1} justifyContent="space-between">
  <Text bold>Title</Text>
  <Text dimColor>right side</Text>
</Pane>

Position and size come from the rect and can't be overridden. That's deliberate. The rect is the single source of truth for where a pane lives, both for the border and for the content.

Pane has to be a direct child of Frame (fragments are fine, they get flattened). Anything that isn't a Pane is ignored.

Reading a pane's usable space

interiorOf gives you the area inside a border, which is the rect minus its two edges. Handy when a child needs to know how much room it really has:

import {interiorOf} from '@oliveryasuna/ink-frame';

const inner = interiorOf(main); // {left, top, width, height} of the content area

When a box is too small to have an interior, the width or height comes back as 0, and Frame skips drawing content into it.

Sizing

Sizes passed to splitColumns / splitRows are either a fixed number of cells or 'grow'. The fixed ones are taken first and the 'grow' panes divide up what's left evenly.

If the fixed sizes don't fit, they scale down instead of overflowing. A terminal narrower than your sidebar shrinks the sidebar rather than shoving panes off the edge of the screen, which is almost always what you want when someone drags their window small.

splitColumns / splitRows overlap adjacent rects by one cell so neighbours share a border. packColumns / packRows take the exact same arguments but butt panes edge to edge with no shared cell, so the sizes sum to the full width or height. Reach for those with a noBorders frame (see below).

Border styles

borderStyle takes 'single' (the default), 'bold', 'double', or 'none', and borderColor takes any Ink color:

<Frame width={width} height={height} borderStyle="double" borderColor="green">

The style applies to the whole frame, junctions included.

No borders

borderStyle="none" still paints the border ring, just as spaces, so content stays inset by a cell on every side. When you want no borders and no space reserved for them, pass noBorders instead:

<Frame width={width} height={height} noBorders>

Now each pane's content fills its whole rect. Reach for packColumns / packRows to lay those rects out, not splitColumns / splitRows: the split functions overlap neighbours by a cell to share a border, and with noBorders that shared cell turns into a cell of overlapping content. The pack functions butt panes edge to edge, so nothing collides.

API

  • Frame: draws the border grid and lays out panes. Props: width, height, borderColor?, borderStyle?, noBorders?.
  • Pane: one bordered box. Props: rect, plus any Box prop for its content.
  • splitColumns(rect, sizes): splits a rect into a row of rects that share borders.
  • splitRows(rect, sizes): splits a rect into a column of rects that share borders.
  • packColumns(rect, sizes): like splitColumns, but the rects butt together with no shared cell.
  • packRows(rect, sizes): like splitRows, but the rects butt together with no shared cell.
  • interiorOf(rect): the area inside a rect's border.
  • Rect, PaneSize: the types.

The return types of the split and pack functions are tuples the same length as the sizes you pass, so const [a, b, c] = splitRows(...) destructures cleanly with no possibly-undefined checks.

Example

There are runnable ones in examples/. example.tsx puts every bordered feature on a single screen: nested splits, a hand-built inset box, fixed and growing panes, and the junctions falling out on their own. example-noborders.tsx shows the noBorders frame with packColumns / packRows.

bun examples/example.tsx
bun examples/example-noborders.tsx

Contributing

Fully AI-generated pull requests are not accepted. You can use AI, but should be verified and cleaned up by a human. Only Opus 4.6+ (high-effort) and Codex 5.4+ (extra high) are accepted models. Preferably created with Opus and verified by Codex. This blurb is adapted from Ink.

I think this is a reasonable requirement, particularly for a tiny library like this. If you think it's too strict, please open an issue and let me know why.

License

MIT © Oliver Yasuna