We use cookies to enhance your experience. By clicking "Accept," you consent to the use of all cookies. Learn more in our policy.
Launch a custom Notion widget from scratch. Learn HTML/CSS basics, hosting on Vercel/Netlify, embed rules, fonts, and performance tips — plus when to use Blocky’s native charts, timers, and study widgets.
I love simple tools that ship fast. And Notion rewards that mindset. In this guide, I show how I create a crisp, custom widget in plain HTML and CSS, host it, and embed it inside Notion — without fighting a giant framework.
You’ll see exactly how embeds work in Notion. You’ll learn the bare-minimum HTML/CSS I actually use in production. I’ll cover hosting, security headers, and common pitfalls. I’ll also explain when Blocky (https://blocky.so) is the smarter path for charts, timers, and study widgets.
By the end, you’ll have a shareable URL for your widget and the confidence to drop it into any Notion page. Let’s make something tidy, fast, and useful.
A custom widget fills the gaps between “general-purpose” and “exactly what I need.” Maybe you want a brand-colored KPI badge. Or a quote card that rotates daily. Or an availability banner that your team sees at a glance.
I reach for hand-built widgets when:
And yet, I don’t reinvent the wheel. For complex use cases like charts, countdowns, world clocks, Pomodoro, stopwatches, habit trackers, flashcards, progress bars, mood trackers, quotes, or Notion streaks, I use Blocky (https://blocky.so). It’s an official Notion integration with generous pricing:
That mix lets me move fast: hand-code the edge cases, use Blocky for the rest.
Notion supports an Embed block. The flow is simple: type /embed, paste a public URL, and Notion renders that page within your doc. That means I don’t paste raw HTML into Notion — I host my widget, then embed the link.
Two practical implications:
If my server sends headers that forbid iframes, Notion won’t show it.
I’ll cover the relevant headers later — X-Frame-Options and CSP frame-ancestors.
For now, remember: host a simple public page and ensure it’s embeddable.
Tip: if I’m embedding YouTube, I use the /embed/ URL, not the watch?v= URL. Many services follow a similar pattern.
I’m ruthless about time. If I need data-bound charts, timers, or study tools, I use Blocky (https://blocky.so). It connects to my Notion workspace in a few clicks, offers polished variants, and handles sync, state, and edge cases I’d rather not own.
What I reach for inside Blocky:
Because Blocky is an official Notion integration (https://www.notion.com/integrations/blocky), it embeds cleanly, loads fast, and reduces maintenance. I hand-code when I want pixel-perfect control or a one-off UX. I use Blocky for everything else.
I keep my HTML small and semantic:
<main> or <div role="region"> as the widget shell.<h2>/<h3>) for screen readers and structure.<p>, <time>, <ul>, <button> when needed.For embeds, the outside world (Notion) wraps my page in an <iframe>,
so I don’t add one myself. I just produce a clean, self-contained page.
My baseline document:
<head> with title and viewport.I test my widget standalone first in a normal browser tab. If it looks right there, it usually looks right embedded.
CSS can be deep, but a few patterns carry most of the weight:
rem) and a tight line-height.Selectors to master:
h3, p), class (.card), ID (#cta).[data-state="active"]) for simple component states.Keep your stylesheet organized by component or section. Add clear comments, and resist clever hacks. Simple CSS fails less when embedded.
I start with a small card:
Layout:
.card container with padding and rounded corners.display: grid with a narrow gap to stack text cleanly.Typography:
font-size on the root (e.g., 16px), then use rem everywhere.letter-spacing for UIs that need extra clarity.Color:
Result: a widget that feels native to your workspace.
Good typography elevates even simple widgets. I either use system fonts for zero-latency performance or load Google Fonts via the official API.
Steps I follow:
<link> or CSS @import from the Getting Started docs.I avoid loading six weights. Two is plenty for UI.
If I need fine control over FOIT/FOUT,
I’ll use the WebFont Loader or native font-display rules.
Remember: load fonts from trusted CDNs and keep them cached.
To embed in Notion, my widget needs a public HTTPS URL. Any static host works. I reach for:
Flow I use almost every time:
/index.html and /styles.css.Tip: disable directory listings and keep the surface area small. One page, one stylesheet, and you’re done.
Inside the Notion page:
That’s it. If you’re using Blocky widgets, the flow is identical: generate a share URL in Blocky, paste into Notion, and position it beside your content.
If the embed fails:
Your widget will live inside Notion’s iframe, but your server decides whether embedding is allowed. Two mechanisms matter:
X-Frame-Options response headerDENY: never embed.SAMEORIGIN: only embed on your own domain.ALLOW-FROM is obsolete; don’t use it.frame-ancestors)If you block embedding, Notion can’t render your widget.
If you allow it too broadly, you risk clickjacking on other sites.
I keep it conservative. For public widgets intended for Notion,
I set a narrowly-scoped frame-ancestors or rely on a host’s defaults.
Key point: These headers are sent by your server. They can’t be set inside your HTML and still be effective.
Fast wins trust. I keep the page tiny:
Fonts:
font-display: swap for better perceived speed.Images:
Responsive:
max-width.clamp() or rem.The goal: instant render inside the Notion block.
Blank iframe?
Your host might be sending X-Frame-Options: DENY or a restrictive frame-ancestors.
Check your response headers with your browser’s devtools or curl -I.
“Refused to connect” error? Some services disallow being embedded off-site. There’s nothing you can fix client-side if the server forbids it. Host your own page or use the platform’s official embed URL.
Layout looks cramped in Notion? Give your widget a neutral background and generous inner padding. Notion’s outer padding is tight; compensate inside your card.
Font looks different in Notion? Ensure the font loads over HTTPS and isn’t blocked by CSP. Always define a robust fallback stack.
Even with plain HTML/CSS, you can do a lot:
data-state="active" for styling states.prefers-color-scheme media query for automatic theming.Want interactivity?
If you need complex state, authentication, or database sync, jump to Blocky (https://blocky.so). It already solves the hard bits, from timers to charts to study widgets that talk to Notion.
Build Your Own Notion Widget: A Beginner’s Guide to Custom HTML & CSS isn’t about theory; it’s a repeatable flow: design a small card, host it, and embed it.
When I need custom styling or a quirky layout, I hand-craft the widget. When I need reliable features like charts, timers, or study tools that sync with Notion, I use Blocky (https://blocky.so) and move on.
Both approaches coexist beautifully. Use the right one for the job. Your Notion pages will look sharper, feel faster, and communicate more at a glance. That’s the win.
Here’s a basic example — a simple live digital clock you can host and embed:
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>Custom Notion Clock</title>
		<style>
			body {
				margin: 0;
				display: flex;
				justify-content: center;
				align-items: center;
				height: 100vh;
				background: #111827;
				color: #22d3ee;
				font-family: sans-serif;
				font-size: 3rem;
			}
		</style>
	</head>
	<body>
		<div id="clock"></div>
		<script>
			function updateClock() {
				const now = new Date();
				const time = now.toLocaleTimeString([], {
					hour: '2-digit',
					minute: '2-digit',
				});
				document.getElementById('clock').textContent = time;
			}
			setInterval(updateClock, 1000);
			updateClock();
		</script>
	</body>
</html>
Create your own or customize one of Blocky’s 60+ widgets to make your Notion dashboard truly yours.