Weblin.org is a lightweight HTML-first architecture for fast web applications.
Characteristics:
HTML-firstAttribute-drivenClasslessFastWeb-nativeCSS as first layerMinimal JSStreamingIslandsLazy loadingNo buildNo virtual DOMNo compilationProgressive enhancementLayouts
Three parts. They can be used separately...
Vist CSS framework
Classless, attribute-driven. Write semantic HTML, add [primary], [flat], etc. — no class names.
Three differentiators: an attribute API (<button primary>, not class="btn btn-primary"), a restricted warm-minimal palette, and native HTML5 patterns (dialog, popover, invokers, view-transitions) as first-class citizens.
Client microframework
HTML-first reactive micro-framework in a single file, no dependencies, no build. Inspired by Fixi, it rewrites the essence of Petite-Vue.
All state at window._
Granular reactivity
define-components for inline or external (SFC) declarative definitions
Servit backend
A template-first micro-server for Bun. Convention-based routing, Eta templates, SQLite sessions, WebSocket, SSE, file uploads, CSRF protection, rate limiting, telemetry — all in ~380 lines.
Zero dependencies
PHP-style developing
BSD-2-Clause
CSS Framework
Motivation
Why does Weblin exist? The design rationale behind every decision — attributes over classes, a restricted palette, native HTML5, zero build, opt-in adaptive performance.
Then write semantic HTML with attributes for variants. No classes, no build step.
Reactivity: Weblin pairs with Menut, the sibling HTML-first micro-framework, for reactive directives and single-file components. If you use Menut under a Content-Security-Policy, its script-src must include 'unsafe-eval' (expressions are compiled via new Function).
Adaptive performance
Optionally pair Weblin with Obs.js to automatically adapt to your user's device. It detects connection strength, battery level, CPU cores, and RAM, then adds classes like has-delivery-mode-lite or has-device-capability-weak to <html>. The companion obs.css reads those classes and flattens shadows, kills animations, or strips backdrop blur on weaker hardware.
If Obs.js is absent or the browser doesn't support the APIs, obs.css has zero cost.
Add the Obs.js script and the stylesheet in your <head>:
The obs.js script must run early in <head>, before any stylesheets or DOM. The obs.css file is a regular stylesheet — include it after css.css. Read more at csswizardry.com/Obs.js.
View transitions
Weblin bakes in the View Transitions API at the CSS level. No JavaScript is needed for the default behavior — just semantic HTML.
Cross-document (MPA)
Same-origin navigations in Chromium-based browsers trigger an automatic cross-fade. Safari and Firefox skip the transition gracefully — content still loads.
@view-transition { navigation: auto; }
/* Optional: name elements that morph across pages */
[vt="hero"] { view-transition-name: hero; }
[vt="title"] { view-transition-name: title; }
Just-in-time naming (js/vt.js)
For list-to-detail morphs (a product grid where each card expands into a hero), don't name every card up front. The optional js/vt.js helper names only the element being navigated from or to, at the moment navigation happens.
<!-- grid page: each card links to a detail -->
<a href="/product/42" vt-jit>Widget</a>
<!-- detail page: the hero, named from the URL -->
<img src="42-hero.jpg" alt="Widget" vt-jit>
<script src="js/vt.js"></script>
Bare [vt-jit] derives the view-transition-name from the URL on both sides, so no shared state is needed. Use vt-jit="name" for an explicit name. It hooks pageswap / pagereveal, cleans up after the transition, and is a silent no-op under prefers-reduced-motion.
In-document (SPA-like)
Tag unique elements with [vt="name"] and trigger via JS:
<h1 vt="title">My title</h1>
<img src="..." vt="image" alt="..." />
<script>
document.startViewTransition(() => {
// update the DOM
});
</script>
Available presets: [vt="hero"], [vt="title"], [vt="logo"], [vt="image"], [vt="modal"], [vt="sidebar"]. Each name must be unique per page. Opt out with [vt-none].
[vt-class] — shared animation styles
While [vt] assigns a unique identity (one element per page), [vt-class] assigns a shared animation class so hundreds of items can use one ::view-transition-group rule.
Available classes: fade, scale, slide-up, slide-left, slide-right. Pair with [vt] for identity + style.
Menut :transition directive
On a reactive re-render, any element marked :transition opts into the View Transitions API automatically — no document.startViewTransition() call needed.
<div :transition>…</div> <!-- native cross-fade -->
<div :transition.hero>…</div> <!-- explicit name -->
<div :transition.fade>…</div> <!-- fade class -->
<div :transition.scale>…</div> <!-- scale class -->
<div :transition.slideup>…</div> <!-- slide-up class -->
<div :transition.shared="avatar">…</div> <!-- morph old/new pair -->
See the Menut docs for full details. All transitions honor prefers-reduced-motion.
Interactive gallery with every option.
CSS locality of behaviour
When a component needs its own styles without polluting global scope, use the native @scope at-rule inside an inline <style> block. Zero JS. No MutationObserver, no querySelectorAll.
Use to ([attr]) to punch a hole — portals, dialogs, and nested components stay unstyled.
Baseline: Chrome 143+, Firefox 146+, Safari 26.2+ (December 2025). For global, reusable styles, keep them in css.css as attribute rules — @scope is for local, component-level styling only.
Elements
Accordion
Native <details> — no JS required.
Accordion 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque urna diam, tincidunt nec porta sed, auctor id velit.
Accordion 2
Vestibulum id elit quis massa interdum sodales.
Nunc quis eros vel odio pretium tincidunt nec quis neque.
Avatar
<img avatar> — circular crop.
Circular avatar via [avatar] attribute. Adjacent avatars overlap automatically.
Badge
Soft-tint pills via attributes. Use [solid] for the black variant.
DefaultSolidsuccesswarningdangerinfo
Mark
Hand-drawn highlighter effect via [highlight]. The SVG filter for rough edges is embedded in css.css — no extra markup.
Uses display: inline-block for positioning — works best on 1–3 words. Line wrapping breaks the effect.
Breadcrumb
<nav breadcrumb> — separator via the breadcrumb attribute.
Button
Default is solid black with a subtle shadow. <button primary> is the single accent button per page with a prominent shadow — use it for the one action that matters most.
Round modifier
Sizes
FAB — floating action button
[fab] shapes a round floating-action button — it does not position it. Drop it in a container you position (like the #backToTop button pinned to the top-right of this page).
Full width
Card
<article> — white surface on the warm-grey page, 16px radius, soft shadow. Use [inverted] for the dark testimonial style, [borderless] for shadow-only, [flat] for border-only.
Standard card
Updated 2 hours ago
Nullam dui arcu, malesuada et sodales eu, efficitur vitae dolor.
Inverted card
For testimonials & highlights
"Best framework I've used in years. The restraint is the feature."
Borderless
Shadow only
Etiam venenatis nisl ut orci consequat, vitae tempus quam commodo.
Checkbox
Dialog
Native <dialog>. Close with form method="dialog" or .close().
Drawer
<dialog drawer> — slides up from the bottom.
Form
Use [field] to group label + input + hint. Native HTML5 validation triggers :user-valid / :user-invalid states.
Group
<fieldset role="group"> — joined inputs edge-to-edge. The input takes available space; the button stays on the same row.
Input
Inputs are width:100% by default. Use size="xxs|xs|s|m|l|xl|auto" to control width.
type="text" preserves leading zeros. inputmode="numeric" triggers the number pad. autocomplete="one-time-code" enables autofill from SMS/password managers. pattern + required enables native validation.
Label
label-float — floating label
Wrap an <input placeholder=" "> or <textarea placeholder=" "> inside <label label-float text="Label">. The label text lives in the text attribute and floats up on focus or when the field has content. No extra markup or JS required.
Description lists
Aliquam lobortis lacus eu libero ornare facilisis.
Nam et magna at libero scelerisque egestas.
Suspendisse id nisl ut leo finibus vehicula quis eu ex.
Description list <dl horizontal> — key-value pairs with label on the left, value on the right.
Type
Mountain
Elevation
8,849 m
First ascent
May 29, 1953
Location
Nepal / China
Loading
aria-busy="true" dims the region and blocks pointer interaction. aria-busy="spinner" adds a spinner.
Menubar
<menu vertical> for vertical:
Navigation Menu
Popover
Native popover API + anchor positioning. Use popovertarget on a button.
Simple popover with anchor positioning.
Dimensions
Set the dimensions for the layer.
Progress
Indeterminate:
Pulse
Attention-grabbing glow animation via [pulse]. Expands a box-shadow ring outward on a 1.5s loop. Great for highlighting new features, notifications, or CTAs.
Add [pulse] to any element
Shine
Shimmering sweep across text via [shine]. A light band travels over a background-clip: text gradient on a 2s loop. Great for hero titles, sale badges, or anything you want to feel alive.
Shiny headlineAdd [shine] to any text
Radio Group
Scroll Area
[scroll-x] / [scroll-y]
Segmented
<fieldset segmented> — pill-shaped radio toggle. Checked option gets var(--fg) fill with var(--shadow-accent). Radio inputs are visually hidden; labels act as the toggles.
Custom-styled native select with chevron icon. For the advanced appearance: base-select approach (custom dropdown with icons, grouped options, animations), see this demo — it requires Chrome 137+ and a different HTML structure.
Separator
Radix Primitives — An open-source UI component library.
<aside> with nested <nav>. Active link via [active].
Skeleton
[skeleton] — shimmer placeholder. Children are hidden. [shimmer] applies the same shimmer to any element without hiding its content.
This should NOT be displayed
This paragraph keeps its text and gets the shimmer pulse. [shimmer] — content stays visible.
Slider
Gradient track + long-shadow thumb. Wrap in [slider-wrap] with an <output> to show the value following the thumb.
With different value:
Sonner
Stacked toasts via <dialog sonner> + <li> children.
Scheduled: Catch up
Friday, February 10, 2023 at 5:57 PM
Layouts
Attribute-driven layout primitives. Drag the right edge of any demo to test responsive behavior.
[stack] — vertical
Item 1
Item 2
Item 3
[rows] — horizontal wrap
A
B
C
[cluster] — flex wrap with gap
Tag
Another
Thing
More
Extra
[repel] — push apart, stacks when narrow
Logo
NavAboutContact
[switcher] — row → column at threshold
Sidebar
Main
[split] — sidebar + main
Sidebar
Fixed 10rem width. Full width when stacked.
Main content
Takes remaining space. Stacks below 30rem.
[center] — centered with max-width
Centered content (max 60ch)
[prose] — article spacing
Heading
Paragraph with automatic spacing. No manual margins needed between elements.
Another paragraph. The gap adapts based on the element type that follows.
Subheading
More content. Headings get more space above them than paragraphs do.
[grid] — auto-fit columns
1
2
3
4
[cols] — fixed columns
A
B
C
Switch
<input type="checkbox" switch> — pill toggle, no JS.
Table
Table caption
Person
Most interest in
Age
Chris
HTML tables
22
Dennis
Web accessibility
45
Sarah
JavaScript frameworks
29
Karen
Web performance
36
Average age
33
Sticky header + first column: <table sticky sticky-col>.
Employee
Department
Role
Location
Tenure
Projects
Alice Nguyen
Engineering
Staff Engineer
San Francisco
6 yr
Platform API
Bob Martinez
Design
Principal Designer
New York
8 yr
Design System
Ciara O'Brien
Product
Product Manager
Dublin
4 yr
Mobile App
Dmitri Volkov
Engineering
Senior Engineer
Berlin
3 yr
Data Pipeline
Elena Santos
Marketing
Marketing Lead
São Paulo
5 yr
Brand Refresh
Fatima Al-Rashid
Engineering
Engineer
Dubai
2 yr
Search
George Kim
Sales
Account Executive
Seoul
7 yr
Enterprise
Hannah Müller
Design
UX Designer
Zurich
3 yr
Accessibility
Ishita Patel
Engineering
Staff Engineer
Bangalore
9 yr
Infrastructure
Jake Thompson
Support
Support Lead
Austin
4 yr
Customer Portal
Keiko Tanaka
Engineering
Senior Engineer
Tokyo
6 yr
Mobile SDK
Liam O'Connor
Finance
Financial Analyst
London
2 yr
Budget Tool
Tabs
CSS-only tabs: radio inputs in a [tabbar] scroller, sections as siblings. Label via data-label. Up to 25 tabs; the tab bar scrolls horizontally when overflowing with a scroll-driven shadow cue. The onwheel handler converts vertical trackpad/scroll-wheel input into horizontal scrolling — if |deltaY| > |deltaX| it calls preventDefault() (so the page doesn't scroll) and adds deltaY to scrollLeft, making horizontal tab bars feel natural on laptops with vertical-only scroll wheels.
Overview
A bunch of info here. The tab panel is a sibling <section> of [tabbar].
Details
More info here. The matching section is shown via :has() by position.
CSS-only via [tooltip-top|bottom|left|right]. Works on hover and focus.
Accesskey
Elements with accesskey get a subtle key-letter badge if the attribute show-accesskey is defined. On hover or focus, it expands to show the full shortcut. Pure CSS — uses attr(accesskey).
Browser behavior varies: Alt+key on Windows/Linux, Ctrl+Option+key on Mac. Check your browser settings if shortcuts conflict.
Ticker
CSS-only horizontal scrolling marquee via [ticker]. Container clips overflow, first child scrolls from right to left. Speed controlled by animation-duration.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Slower: style="animation-duration:30s"
Slower scroll — this text takes 30s to cross the viewport.
Selectable card
<article selectable> with a child <input type="checkbox">. Uses :has() to highlight when checked. Click the checkbox or the card.
CSS helpers
Native browser capabilities wired up as attribute helpers — no JS. CSS-only additions to css.css.
Native dialog — open without JS
Plain <dialog> (already styled) opened with commandfor + command, and dismissed natively with closedby="any" (backdrop click or ESC).
<button commandfor="dlg" command="show-modal">Open</button> + <dialog id="dlg" closedby="any">. command accepts show-modal/show-popover/toggle-popover/close. The styled <dialog> is fully defined in css.css — no JS or component required.
lazy-render — render on demand
[lazy-render] on a section skips rendering/layout until it nears the viewport (content-visibility: auto + intrinsic-size hint). Pairs well with [reveal] for below-fold content.
Revealed on scroll: this block fades and slides up when it enters the viewport via [reveal] (scroll-driven animation, Chrome 115+).
carousel — CSS-only scroll-snap with arrows & dots
[carousel] gives you a scroll-snap carousel. In supporting browsers (Chrome 115+, Safari 17.4+), prev/next arrows (::scroll-button) and snap-tracking dots (scroll-marker-group / ::scroll-marker) appear automatically. Unsupported browsers keep the plain scroll-snap carousel.
Beyond the attribute-based [tooltip-top|bottom|left|right] text tooltips, you can have rich HTML content in a tooltip using popover=hint + interestfor. Hover or focus the buttons below.
Rich tooltip
This tooltip uses popover=hint — it shows on hover/focus, closes on outside click/ESC, and supports full HTML content.
Also works on focus
Tab to the second button — the tooltip appears on focus too. Great for accessibility.
Baseline
Foundation-level CSS features used throughout.
scrollbar-gutter: stable reserves scrollbar space even when content doesn't overflow — no layout shift when a page transitions from short to tall. Set on body so every page has consistent gutter width.
text-box-trim: trim-both + text-box-edge: cap alphabetic trims whitespace above the cap height and below the baseline. This gives true cap-height centering on buttons and inline elements — no more guessing with line-height hacks.
Print
Sheet-level control and attribute helpers for printing. @page sets the physical paper (default A4 portrait, margin: 0 — switch to Letter portrait for US). Margins live in the DOM (padding), not @page, so on-screen preview matches the printed sheet.
no-print / print-only
Attribute-driven show/hide for print: [no-print] is hidden when printing, [print-only] is hidden on screen and shown when printing.
This line only appears when printing.
Paging
[page-break-before] and [page-break-after] force breaks; [avoid-break] keeps an element (e.g. a card) from splitting across pages.
Stays together
Mark me with avoid-break so I don’t split across printed pages.
Printing also forces an ink-friendly appearance: white background, black text, no shadows or backdrop filters, color-scheme: light.
Pre-composed layouts
Layout primitives combine into reusable page sections. These are the seeds for a future community-layouts library.
The palette is deliberately small: a warm light-grey page, white card surfaces, near-black text, and three accent families (green / yellow / red) each in two tiers — soft tints for badges, saturated for action buttons.
A 4.5:1 display-to-body ratio. Display headlines are tight-tracked; body is relaxed.
Display
Design insights that grow startups
Headings
H1 — 48px
H2 — 36px
H3 — 28px
H4 — 22px
H5 — 18px
H6 — 16px
Body & inline
Body text at 16px with a 1.6 line-height. Bold, italic, link, small, kbd, code.
"Maecenas vehicula metus tellus, vitae congue turpis hendrerit non."