Web Design
A Recipe Card That Prints on One Page
Part 6 of the thread Building this notebook
- Recipes here are structured front matter, so the template can draw a proper ingredients-and-directions card with checkboxes.
- The Print button clones the title, the quick facts and the card into a
#print-sheetat the end ofbody, adds a class, then callswindow.print(). - A print stylesheet hides every other child of body with
display: noneand tightens the type.afterprintcleans it all up. - My first try used
visibility: hidden. Hidden things still take up space, so the printout came out five pages long.
Every recipe site has the same problem. You're standing in the kitchen, you hit print, and out comes the header, the navigation, a sidebar, somebody's life story, and somewhere on page three, the ingredients.
The recipes on this site print differently. Hit Print recipe on something like Gram's white layer cake and you get the title, the quick facts, and the card. That's it. Here's how it works, including the version that didn't.
Start with structured data
None of this works if the recipe is just a blob of Markdown. The ingredients and steps live in front matter, grouped:
recipe:
serves: 16 to 20
prep: 45 minutes
oven: 350°F
ingredients:
- group: Cake layers
items:
- "3 cups all-purpose flour"
- "8 large egg whites, room temperature"
steps:
- group: The cake
items:
- "Heat the oven to **350°F**."
Because it's data, the post template can render it however it wants. It draws a recipe card: ingredients on the left, each with a checkbox you can tick off as you go, and numbered directions on the right. The quick facts (servings, prep, cook, heat) sit in a summary box near the top of the post.
The Print buttons ship in the HTML with the hidden attribute. The script un-hides them. No JavaScript, no button, and the browser's own print still works.
The print button builds a sheet
When you click Print, the script doesn't try to hide its way down to the card. It builds a fresh little page and hangs it off the end of body:
const card = document.querySelector('.recipe-card');
if (card) {
const sheet = document.createElement('div'); sheet.id = 'print-sheet';
const h = document.createElement('h1'); h.textContent = document.querySelector('.article-header h1')?.textContent || document.title; sheet.append(h);
const facts = document.querySelector('.recipe-summary dl'); if (facts) sheet.append(facts.cloneNode(true));
sheet.append(card.cloneNode(true));
document.body.append(sheet); document.body.classList.add('print-recipe-only');
}
window.print();
Three things go in: the post title, a clone of the quick-facts list, and a clone of the card. Then a class goes on body and the print dialog opens.
Why clone instead of printing the real card? Because the real card lives deep inside the article, inside main, inside the layout. To show it, every one of its ancestors has to stay visible, and then you're back to fighting the whole page. A copy that's a direct child of body has no ancestors worth worrying about.
The print stylesheet hides everything else
With the sheet in place, the CSS is almost boring:
@media print {
body.print-recipe-only { background: #fff !important; }
body.print-recipe-only > *:not(#print-sheet) { display: none !important; }
#print-sheet { display: block; color: #000; font-family: Arial, sans-serif; }
#print-sheet .script-bar { display: none; }
#print-sheet .recipe-ingredients input { display: none; }
#print-sheet .recipe-steps li { font-size: 10pt; line-height: 1.35; padding-bottom: 4px; }
.recipe-card li { break-inside: avoid; }
}
Every child of body that isn't the sheet gets display: none. The window bar and the checkboxes drop out of the copy. Font sizes switch to points, the padding tightens, and break-inside: avoid keeps a single step from splitting across a page break if a long recipe does spill over.
There's one nice extra. A line with the post's URL sits at the bottom of every card, hidden on screen and shown in print, so a paper copy stuck to the fridge still says where it came from.
Because the hiding rules are keyed to body.print-recipe-only, pressing Ctrl+P on its own still prints the whole post. The one-page version only happens when you ask for it.
Clean up afterwards
The browser fires afterprint once the dialog closes, whether you printed or cancelled:
window.addEventListener('afterprint', () => {
document.body.classList.remove('print-recipe-only');
document.getElementById('print-sheet')?.remove();
});
Class off, sheet gone, page back to normal.
Why not visibility: hidden?
That was the first attempt, and it's the trick you'll find in a lot of answers online: set visibility: hidden on everything, then visibility: visible on the one element you want. It's attractive because visibility can be switched back on for a child even when the parent is hidden, which display can't.
The catch is in the name. Hidden isn't gone. An element with visibility: hidden still takes up exactly as much room as it did before. The header, the summary box, the short-version box, the prose, the related posts: all invisible, all still there, all still pushing the card down the page.
So the first printout was five pages, mostly blank, with the recipe floating somewhere in the middle. Technically correct. Completely useless in a kitchen.
| Approach | What the hidden stuff does | Result |
|---|---|---|
visibility: hidden on everything but the card |
Invisible, but keeps its space | Blank pages around the recipe |
display: none on the card's ancestors |
Removed, and takes the card with it | Nothing prints |
Clone to #print-sheet, display: none on the rest |
Removed from layout entirely | Title, facts and card, starting at the top |
Tip
When you want to print one piece of a page, move or copy it somewhere you can isolate it rather than hiding your way down to it.
display: nonereally removes things.visibility: hiddenonly makes them invisible.
Is it always one page?
That's the goal, and the tight print type is sized for it. A really long recipe will still run to a second page; no stylesheet can fold paper. When it does, at least it's two pages of recipe.