Notebook Page
A sheet of notebook paper behind what someone writes: rules, dots or squares spaced on the text's own line height, so every line of a note sits on one. Add the red margin, the spiral binding of a pocket notebook and fields that write straight on the rules, or wrap a Rich Text Editor or plain copy.
Installation#
Usage#
import { NotebookPage } from "@/components/ui/notebook-page"
export function Example() {
return (
<NotebookPage rows={8}>
<p>Everything inside takes the page's line height,</p>
<p>so plain lines of text sit on the rules.</p>
</NotebookPage>
)
}Anatomy#
import {
NotebookBinding,
NotebookPage,
NotebookPageField,
NotebookPageTitle,
} from "@/components/ui/notebook-page"
<NotebookBinding start={…} end={…} />
<NotebookPage>
<NotebookPageTitle />
<NotebookPageField />
</NotebookPage>Lines#
ruled (the default) prints a hairline at the foot of every line, where the writing rests. dotted sets a dot at every line and every line-width step, a bullet journal's grid; grid crosses the rules with columns of the same pitch; plain keeps the rhythm and prints nothing. The rules are the soft border role and the dots a speck of the muted ink, so every pattern re-themes with the page and never competes with what is written on it.
Sizes#
The size picks the line and the type that reads well on it together: sm is 14px type on a 24px line, md (the default) 16px on 28px, lg 18px on 32px. The line is the --notebook-line custom property, the one length everything inside inherits as its line height.
Margin#
margin="left" prints the red rule of a school notebook down the left of the sheet, one and a half lines in from the edge, and starts the writing a quarter line past it. Both are measured in --notebook-line, so the margin keeps its proportion at every size. The inset belongs to the margin: it outranks a plain px-* on the page, so the rule never ends up under the text (pad the right with pr-*, or move the inset on purpose with data-[margin=left]:pl-*). The red is the --notebook-margin token, the error role at under half strength.
Fields#
NotebookPageField writes straight on the rules: a textarea with no frame, no fill and no ring, whose line height is the page's line, so every line typed sits on one and its rows are page lines. Focus shows as the brand caret alone, so a field that always has focus (a pocket notebook's body) never looks tinted; the placeholder is the muted ink. asChild puts the same style on another field (an <input>, a third-party textarea). NotebookPageTitle is the title line: one line, larger and bolder, still exactly one rule tall; an input by default, a heading with asChild. Neither carries a visible label, so give each an aria-label.
Focus tint#
focusTint adds the accent's faintest wash over the lines while a field or the title has focus. Leave it off where one field is the page (a pocket notebook whose body is focused as soon as it opens: the tint would never go away); turn it on where several fields share a sheet and the eye has to find the one being written in.
Binding#
NotebookBinding is the spiral along the top edge of a pocket notebook: metal rings over the shade the binding throws on the sheet, all in the --notebook-ring* and --notebook-binding-shade tokens. rings sets how many; unset, the strip fills with as many whole rings as fit its width, spread evenly, at any width. start and end hold a control or a hint at either end (a close button, a shortcut, a live status). Put it above the page, inside the notebook's frame.
Pocket notebook#
The parts together: a binding with a close button and the shortcut at its ends, a ruled page with the margin, a title line and the note written on the rules, and the actions under the sheet. Type a note and save it: the shortcut hint gives way to a live “Saved”.
With the Rich Text Editor#
Write straight onto the page: drop the editor's frame and fill (the paper is the ground) and tell its blocks to use the line with no gap between them, so each new paragraph lands on the next rule. Content that sets its own line height or margins drifts a little further off the rules with every block; this is the one adjustment it needs.
Rows#
rows gives the page its height in lines, like a textarea's, so a blank note still reads as a page to write on. The page grows past it with its content; unset, it hugs what it holds. Padding never knocks the lines off the text: the pattern starts at the first line of the content box and still runs through the padding.
API reference#
NotebookPage
The sheet, a <div> (or, with asChild, the child element). It sets --notebook-line and the matching type size, so plain lines of text sit on the rules. Every div prop is forwarded; className is merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| lines | "ruled" | "dotted" | "grid" | "plain" | "ruled" | The pattern: hairline rules, a dot grid, squared paper, or nothing printed on the same rhythm. |
| size | "sm" | "md" | "lg" | "md" | The line (24, 28 or 32px) and the type size that reads well on it. |
| margin | "none" | "left" | "none" | left paints the red rule at --notebook-margin-x (1.5 lines) in --notebook-margin and insets the content a quarter line past it with data-[margin=left]:pl-*. |
| rows | number | - | The minimum height in lines, empty or not. Unset, it hugs its content. |
| asChild | boolean | false | Renders the paper onto the child element (an <article>, a <section>). |
| Data attribute | Description |
|---|---|
| data-slot="notebook-page" | On the root. |
| data-lines | The current lines. |
| data-margin | The current margin. |
| CSS variable | Description |
|---|---|
| --notebook-line | The line: the line height of everything inside and the period of the pattern. |
| --notebook-rows | Set from rows. |
| --notebook-margin-x | Where the margin rule sits, with margin="left". |
| --notebook-margin | The margin rule's colour (a token in globals.css). |
NotebookBinding
The spiral along the top edge of a pocket notebook, a <div>. Put it above a NotebookPage inside the notebook's frame. Children go between the rings and end. Every div prop is forwarded.
| Prop | Type | Default | Description |
|---|---|---|---|
| rings | number | - | How many rings the spiral has. Unset, the strip fills with as many as fit its width. |
| start | React.ReactNode | - | A control or a hint at the leading end (a close button). Rendered in data-slot="notebook-binding-start". |
| end | React.ReactNode | - | A control or a hint at the trailing end (a shortcut, a "Saved" status). Rendered in data-slot="notebook-binding-end". |
| Data attribute | Description |
|---|---|
| data-slot="notebook-binding-rings" | The rings, aria-hidden. |
| data-fill | On the rings when rings is unset and they fill the strip. |
NotebookPageField
A <textarea> written straight on the page (or, with asChild, the child field): transparent, no border or ring, line height var(--notebook-line), caret-brand as the focus cue, muted placeholder. Its rows count page lines; an input child is one line tall. Every textarea prop is forwarded. Give it an accessible name: the placeholder is not one.
| Prop | Type | Default | Description |
|---|---|---|---|
| focusTint | boolean | false | A --ring-brand wash on :focus-visible, where several fields share a page. |
| asChild | boolean | false | Styles the child field instead (an <input>, a third-party textarea) and renders no textarea. |
| Data attribute | Description |
|---|---|
| data-slot="notebook-page-field" | On the field. |
NotebookPageTitle
The title line, an <input type="text"> (or, with asChild, the child, e.g. a heading). One rule tall, type at 1.375em and semibold, truncate. Every input prop is forwarded.
| Prop | Type | Default | Description |
|---|---|---|---|
| type | string | "text" | The input type. |
| focusTint | boolean | false | A --ring-brand wash on :focus-visible, as on the field. |
| asChild | boolean | false | Puts the title style onto the child (an <h2> for a page that is read, not written). |
| Data attribute | Description |
|---|---|
| data-slot="notebook-page-title" | On the title. |
notebookPageVariants
The tv recipe behind every part, for styling a part of your own to match.