Components
Each one is a React component on window.DeliPaper, with a dp-* class in bundle.css for pages without React. The previews below run the real bundle.
- CardSurfaces
- PaperSurfaces
- ColumnsLayout
- HeroLayout
- MastheadLayout
- SectionLayout
- CodeText
- CodeBlockText
- MarkerText
- RowsLists
- PhoneFigures
- PhonesFigures
- AmountMoney
- ButtonActions
- TextLinkActions
- BadgeApp screens
- FieldApp screens
- RevealMotion
Surfaces
Card
The one white sheet on the paper: square corners, space-16 inside, card-width wide at most, centred.
Use exactly one per page and put everything readable in it. Nothing goes straight on the paper. Under 720px the padding drops to 32px by space-6.
- Children: the
Masthead, thenmaincontent. as: the element,divby default.- Don't round it, shadow it or nest one inside another. The card is the page.
Paper
The checked deli paper every page sits on: a WebGL sheet, painted once per size and theme, that never moves.
Mount one per page, behind everything, before the Card. It is decorative and hidden from assistive tech.
colourway:"deli"(the portfolio:deli-inkondeli-paper),"ledger"(splitova:ledger-creditandledger-debitchecks with + and − rupee amounts that count up),"hook"(mhook:hook-inkonhook-paper), or your own{ ink, paper }as 0–1 RGB.amounts: print counting amounts in the checks. On by default forledgeronly; it is the only colourway where the numbers mean something.contained: fill the parent (position absolute) instead of the window. Use it for previews and embeds, never on a real page.- The consumer provides nothing else. Give each new app its own colourway rather than reusing another app's, and add its two colours to the tokens as
<app>-inkand<app>-paper. - Without WebGL the paper falls back to the
pagecolour, so text on the card stays legible.
Layout
Columns
Two equal columns with the space-8 gutter, stacking to one under 720px.
Use it for the opening paragraphs of a page, two to a side. Children are the columns, usually a div of paragraphs each. Text inside is set in body.
Hero
The page's one statement: a display headline, a muted lede and the actions, with the app's screens below.
Use it on app sites, not on the portfolio itself. The headline says what the app does in under ten words and wraps at 15ch; the lede is one or two sentences of measure width. One Button (ink) and at most one text link follow.
title,lede: strings or nodes.actions: a fragment ofButtons. Children: the figures, usuallyPhones.- Size it to its content, never to the viewport.
Masthead
The header: a stacked list on the left (the brand or socials) and the nav stacked on the right, both in nav type.
When the page scrolls the nav to the top, it pins there as one row on a card strip with a rule underline, and each link glides to its new place (duration-glide, 30ms apart, ease-out). The left list fades out as it pins, so nothing scrolls over the strip, and the brand from lead slides in at the strip's left end, lined up with the card's content. On a phone only its mark shows, and the pinned row steps down to 16px (14px under 360px) so the links stay clear of it.
lead:[{ label, href, brand? }]. The first item is the app:brandis its mark (an<img>or SVG) shown before the label inheading, with no tile behind it. After it, one or two useful links (socials, the beta, the reader). No byline: the app's own name is enough.nav:[{ label, href, current? }], three or four links.currentmarks the page you are on.pin:falseto keep it static (previews, short pages).- Links are
mutedat rest andheadingon hover or when current. The text size never changes when it pins.
Section
A light title heading and its content, space-30 below the last section (space-18 under 720px).
Give it an id when the nav links to it; it clears the pinned nav when jumped to. Headings are short nouns ("Projects", "Now", "Ways to answer"), sentence case, no colon.
title: the heading text.id: the anchor.style: overrides, such asmarginTop: 0for the first section under a hero. Children: the content.
Text
Code
Inline code in running text: mono at 0.85em on card-sunken with radius-sm.
Use it for names a reader would type: commands, flags, file names. Children: the text.
CodeBlock
A block of code on card-sunken with radius-md, mono at 0.9rem / 1.55, scrolling sideways inside itself.
Show a real, runnable example with the reader's placeholders in $CAPS. Children: the code as a string with its own line breaks.
Marker
The hand-drawn highlighter stroke behind the one line that matters, drawn left to right as it enters the window (duration-draw after 450ms, ease-draw).
Use one per page, two at most. It wraps across lines without breaking its shape.
tone:"red"(deli red, the default: the line you want read),"blue"(a name in running text, withinline),"yellow","hook"(mhook) or"ink"(splitova and other monochrome apps).inline: tighter padding for a word mid-sentence, so the text does not move.draw={false}: show it already drawn.
Lists
Rows
A list of things, a title on the left and its detail on the right in muted, between rule hairlines.
The portfolio's projects, posts and links are all rows. Details are one line: a technical one-liner, a date or a handle. Under 720px the detail drops under the title, unless the rows hold figures.
items:[{ title, detail, href?, key? }]. Ahrefmakes the title aTextLink.figures: the details are numbers or money; they align as tabular figures and stay on the right at every width.
Figures
Phone
One app screenshot in a rounded frame (radius-phone, rule border, shadow-phone) with an optional caption below in muted.
Beside text, use one at 15rem wide in a two-column grid. Screens are iPhone-sized captures (1179×2556 or similar) scaled to 640px wide JPEG.
src,alt(what the screen shows, not "screenshot"),caption,width/height(the image's real size, 640×1387 by default),loading("eager"in a hero).
Phones
Three app screens side by side, space-5 apart, each a Phone; under 720px they scroll sideways and snap.
Use it in a Hero or right after one. Pick screens that show the app working with realistic data, never sign-in screens or QA text. Children: three Phones.
Money
Amount
Signed money in rupees with Indian grouping: credit with a + for money coming back, debit with a − for money owed, plain for zero.
The sign is always written, because credit and debit share a lightness and colour cannot carry it alone. With count, it grows from zero over duration-count (ease-out) when it enters the window.
value: a number in rupees, negative for owed.count: animate.duration: ms.currency={false}: no ₹.label: an accessible name, such as "You're owed ₹7,465".
Actions
Button
Ink is the one call to action on a view; outline is the quiet second; text is a link sitting in the same row.
variant:"ink"(default:headingfill,on-inklabel,radius-pill),"outline"(rule-strongborder) or"text"(aTextLinkstyled as a sibling of the ink button).hrefmakes it a link; otherwise it is abutton type="button". Every other prop passes through.- Labels are a verb and an object, sentence case: "Get the beta", "Sign in", "Add expense". No icons unless the app's own screens use them.
TextLink
A link inside running text: underlined at 35% of the text colour, with a yellow marker that paints in from the left on hover (duration-instant, linear).
Use it for every link inside the card except the header and buttons. Link the words that name the place ("suitecut", "Get in touch"), never "here". Every prop passes through to the a.
A link to another site opens in a new tab (target="_blank", rel="noopener noreferrer"); links within the site and mailto: links open in place. Pass target yourself to override it. For links you don't render with TextLink, such as Markdown, one listener does the same:
document.addEventListener('click', (event) => {
const link = event.target.closest?.('a[href]');
if (!link || link.target || !/^https?:$/.test(link.protocol) || link.origin === location.origin) return;
link.target = '_blank';
link.relList.add('noopener', 'noreferrer');
});
App screens
Badge
A word or two of state in dense app screens, such as the mhook console's "Has hooks": a pill with a dot in its tone.
Intentional addition: the portfolio has no badges; the mhook console needs them for status in tables.
tone:"neutral"(no dot),"credit"or"debit". Children: the words, sentence case. The words carry the state; the dot repeats it.
Field
A label over a text input, with an optional hint or error below.
Intentional addition: the portfolio has no forms; the app sites' sign-in pages need them.
label,id(required, links the label),hint,error(replaces the hint, turns the borderdebitand is announced). Every other prop goes to theinput.- Errors say what to do: "Enter the 6-digit code from the email", not "Invalid input".
Motion
Reveal
Its children rise 14px and fade in as they enter the window, 70ms apart (duration-rise, ease-out, eight steps at most).
Wrap a section's paragraphs, rows or figures. What is already on screen shows at once, and without script or with reduced motion everything is simply visible. Don't wrap the masthead: a transform there stops the nav pinning.
as: the wrapper element. Children: the items that rise, one by one.