Are you looking for an avatar for your web? It's here: weblin.io  
logo
Getting started
Introduction Motivation Installation Adaptive performance View transitions Locality of behaviour
Layouts
Pre-composed layouts →
Menut micro-framework
Menut →
Components
Accordion Avatar Badge Breadcrumb Button Card Checkbox Dialog Drawer Form Group Input Input OTP Label Description lists Loading Mark Menubar Navigation Menu Popover Progress Pulse Shine Radio Group Scroll Area Segmented Select Separator Sheet Sidebar Skeleton Slider Sonner Layouts Switch Table Tabs Textarea Toast Tooltip Accesskey Ticker Selectable card CSS helpers Print
Design tokens & typography
Cheat Sheet

weblin.org

Weblin.org is a lightweight HTML-first architecture for fast web applications.

Characteristics:

HTML-first Attribute-driven Classless Fast Web-native CSS as first layer Minimal JS Streaming Islands Lazy loading No build No virtual DOM No compilation Progressive enhancement Layouts
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.

  1. All state at window._
  2. Granular reactivity
  3. 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.

  1. Zero dependencies
  2. PHP-style developing
  3. 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.


Installation

Include the framework via CDN:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/alsanan/weblin/css.css">

Or download and include locally:

<link rel="stylesheet" href="css.css">

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>:

<script src="js/obs.js"></script>
<link rel="stylesheet" href="obs.css">

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.

<div vt="card" vt-class="fade">…</div>
<div vt="card" vt-class="scale">…</div>

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.

Basic scope

<div feature>
  <style>
    @scope {
      & { background: var(--bg-card); padding: var(--space-4); }
      img { border-radius: var(--radius-card); }
      h3 { color: var(--fg-muted); }
    }
  </style>
  <h3>Scoped title</h3>
  <img src="photo.jpg" alt="…" />
</div>

& targets the host element. Descendants are automatically scoped — styles never leak out, nothing from the outside leaks in.

Donut scope — exclude sub-trees

<div card>
  <style>
    @scope to ([portal]) {
      & { border: var(--border); border-radius: var(--radius-card); }
      h3 { margin: 0; }
    }
  </style>
  <h3>Card title</h3>
  <p>Scoped content.</p>
  <div portal><!-- outside the scope --></div>
</div>

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.

avatar

Circular avatar via [avatar] attribute. Adjacent avatars overlap automatically.

A B C D

Badge

Soft-tint pills via attributes. Use [solid] for the black variant.

Default Solid success warning danger info

Mark

Hand-drawn highlighter effect via [highlight]. The SVG filter for rough edges is embedded in css.css — no extra markup.

Default yellow: highlighted text.

Custom colors via --color: green, red, blue.

<mark highlight>highlighted</mark>
<mark highlight style="--color: var(--color-green)">green</mark>

Uses display: inline-block for positioning — works best on 1–3 words. Line wrapping breaks the effect.



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.

Duis nec elit placerat

Inverted card

For testimonials & highlights

"Best framework I've used in years. The restraint is the feature."

— A. Developer

Borderless

Shadow only

Etiam venenatis nisl ut orci consequat, vitae tempus quam commodo.


Checkbox


Dialog

Native <dialog>. Close with form method="dialog" or .close().

Confirm your action

Cras sit amet maximus risus. Pellentesque sodales odio sit amet augue finibus.

closedby="any"

Click the backdrop or press Esc to close.

This dialog has closedby="any", allowing backdrop-click closing in browsers that support it. In unsupported browsers it behaves like a normal modal.


Drawer

<dialog drawer> — slides up from the bottom.

Drawer

Cras sit amet maximus risus. Pellentesque sodales odio sit amet augue finibus.

Drawer with closedby="any"

Click outside or press Esc to close.

In supporting browsers, clicking the backdrop closes this drawer.

Drawer rounded

border-radius: var(--radius-card) var(--radius-card) 0 0

Drawer full

max-height:100vh; max-width:100vw; width:100vw — edge to edge.

Drawer full rounded

border-radius: var(--radius-card) var(--radius-card) 0 0


Form

Use [field] to group label + input + hint. Native HTML5 validation triggers :user-valid / :user-invalid states.

Press / to focus.
We'll never share your email.
Checkboxes
Radio buttons

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.


Input OTP

Single <input>. No separated inputs, no JS.

<label for="otp">Enter the 6-digit code</label>
<input type="text" otp
  inputmode="numeric"
  autocomplete="one-time-code"
  maxlength="6"
  pattern="\d{6}"
  required
  placeholder="------">

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.




Popover

Native popover API + anchor positioning. Use popovertarget on a button.

Simple popover with anchor positioning.

Dimensions

Set the dimensions for the layer.
  • Rename
  • Duplicate
  • Move to…
  • Delete

  • 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 headline Add [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.

    Code
    <fieldset segmented>
      <label><input type="radio" name="seg-demo" checked /> Day</label>
      <label><input type="radio" name="seg-demo" /> Week</label>
      <label><input type="radio" name="seg-demo" /> Month</label>
      <label><input type="radio" name="seg-demo" /> Year</label>
    </fieldset>

    Select

    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.

    <hr vertical>


    Blog
    Docs
    Source

    Sheet

    <dialog sheet-top|bottom|left|right>

    Modifiers rounded and full:

    With closedby="any" (backdrop click closes):

    Sheet top

    Cras sit amet maximus risus.

    Sheet bottom

    Cras sit amet maximus risus.

    Sheet left

    Cras sit amet maximus risus.

    Sheet right

    Cras sit amet maximus risus.

    Sheet bottom rounded

    border-radius: var(--radius-card) var(--radius-card) 0 0

    Sheet bottom full

    max-height:100vh — extends to full viewport height.

    Sheet right full

    max-width:100vw — extends to full viewport width.

    Sheet top · closedby="any"

    Click the backdrop to close.

    In supporting browsers, clicking outside this sheet closes it.

    Sheet bottom · closedby="any"

    Click the backdrop to close.

    In supporting browsers, clicking outside this sheet closes it.



    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.

    50

    With different value:

    25

    Sonner

    Stacked toasts via <dialog sonner> + <li> children.


    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
    Nav About Contact

    [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
    ChrisHTML tables22
    DennisWeb accessibility45
    SarahJavaScript frameworks29
    KarenWeb performance36
    Average age33

    Sticky header + first column: <table sticky sticky-col>.

    Employee Department Role Location Tenure Projects
    Alice NguyenEngineeringStaff EngineerSan Francisco6 yrPlatform API
    Bob MartinezDesignPrincipal DesignerNew York8 yrDesign System
    Ciara O'BrienProductProduct ManagerDublin4 yrMobile App
    Dmitri VolkovEngineeringSenior EngineerBerlin3 yrData Pipeline
    Elena SantosMarketingMarketing LeadSão Paulo5 yrBrand Refresh
    Fatima Al-RashidEngineeringEngineerDubai2 yrSearch
    George KimSalesAccount ExecutiveSeoul7 yrEnterprise
    Hannah MüllerDesignUX DesignerZurich3 yrAccessibility
    Ishita PatelEngineeringStaff EngineerBangalore9 yrInfrastructure
    Jake ThompsonSupportSupport LeadAustin4 yrCustomer Portal
    Keiko TanakaEngineeringSenior EngineerTokyo6 yrMobile SDK
    Liam O'ConnorFinanceFinancial AnalystLondon2 yrBudget 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.

    Edit

    View the markup
    <div tabs>
      <div tabbar onwheel="e=event; if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { e.preventDefault(); this.scrollLeft += e.deltaY; }">
        <input type="radio" name="mytabs" data-label="Tab 1" checked>
        <input type="radio" name="mytabs" data-label="Tab 2">
      </div>
      <section>Panel 1</section>
      <section>Panel 2</section>
    </div>

    Textarea


    Toast

    Single toast via <dialog toast>.

    Scheduled: Catch up
    Friday, February 10, 2023 at 5:57 PM

    Tooltip

    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.

    Native dialog

    Opened via commandfor + command="show-modal" — no JS. Click the backdrop or press ESC to close (closedby="any").


    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.

    Slide 1
    Slide 2
    Slide 3
    Slide 4
    Slide 5
    Slide 6
    Slide 7
    Slide 8
    Slide 9
    Slide 10
    Slide 11
    Slide 12
    Slide 13

    tooltip — rich hover tooltips (popover=hint + interestfor)

    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.


    Dev tools

    HTML integrity tests — CSS-based checks for markup anti-patterns (based on REVENGE.CSS).


    Design tokens & typography

    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."
    — Phasellus eget lacinia

    Cheat Sheet