SuCSS

Just HTML, already styled.

SuCSS is a classless CSS framework. Write plain HTML — <header>, <table>, <button> — and liquid-glass surfaces, dark mode and accessibility come with it. No class names, no custom attributes, no build step.

This page is the demo. It carries no class attribute anywhere. Paste the line below into your <head> and your own HTML looks like this.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryo9ra/su-css/dist-lib/sucss.min.css">

Su (素) is Japanese for plain, or unadorned.

Features

No class names

Styling comes from the elements themselves. There is no naming convention to agree on and no utility classes to memorise.

Dark mode included

Follows prefers-color-scheme, and data-theme pins it either way.

Accessible by default

WCAG AA/AAA contrast, visible :focus-visible rings, adequate touch targets. It honours prefers-reduced-motion and prefers-reduced-transparency too.

Liquid glass

Surfaces behave like a lens rather than a frosted sheet: the backdrop stays legible through them, colour blooms out of them, and every edge carries a rim of light.

One file, no dependencies

A single stylesheet, 6.6 KB minified and gzipped. No JavaScript, no build step.

Tuned with variables

Every colour, radius and rhythm is a custom property. Change --hue and the whole page moves — try it in the customizer.

Install

For the CDN, paste the line above into your <head>. With a bundler, install it from npm.

npm install @ryo9ra/su-css
import '@ryo9ra/su-css/sucss.min.css';

What ships
Import specifier File Size
@ryo9ra/su-css dist-lib/sucss.css 64 KB (gzip 16.1 KB)
@ryo9ra/su-css/sucss.min.css dist-lib/sucss.min.css 36 KB (gzip 6.4 KB)

Supported browsers are Chrome/Edge 111+, Safari 16.4+ and Firefox 128+. Without backdrop-filter, surfaces fall back to solid fills.

Elements

Everything below is plain HTML with no class attribute. Open the page source and compare.

Text

Paragraphs are set from --line-height and --max-width. Strong, emphasis, bold, italic, underline, strikethrough, inline code, Ctrl + C, CSS, x, output, a defined term, subscript, superscript and a link all come styled.

The hours you would spend designing class names go into designing the semantics instead.

— on going back to plain HTML

Heading level 4

Heading level 5
Heading level 6

Landmark roles

Landmarks are styled by their role as well as by their tag: banner, navigation, main, complementary, contentinfo, region and search. Markup that carries the role on a <div> gets the surface of the element it stands for.

<div role="complementary">

Painted by the rule that draws the <aside> above it — no class, no extra CSS.

<search>

Lists

  1. Load one line of CSS
  2. Write plain HTML
  3. That is the whole method
Classless
Styling a page through element selectors alone, with no class attributes.
Liquid glass
A surface that lets the backdrop through, blooms its colour, and carries light on its edges.

Tables

The main custom properties and their defaults
Property Default What it moves
--hue 262 The accent colour and the background mesh
--sat 85% Saturation of the accent colour
--max-width 860px Width of the body column
--radius 1.375rem Corner radius of surfaces
--glass-blur 14px How far the glass blurs its backdrop
All of them can be overridden in :root.
A sortable table: the marker follows aria-sort on the header cell
Element Attribute What it shows
<details> [open] Whether the disclosure is open
<progress> [value] How far along the task is
<th> [aria-sort] Which column the rows are ordered by

The first column carries aria-sort="ascending", so its marker points up; "descending" turns it over. The second is aria-sort="none" and the third has no attribute at all — both are drawn exactly as they were.

Forms

Account
Settings

Buttons and states


A toolbar asks Tab to enter it once and the arrow keys to move inside. SuCSS draws the bar; @ryo9ra/su-css/behaviors.js writes the keyboard part, or write it yourself. Use role="group" when you cannot do either.

Stable MIT Zero dependencies

70%

42%

A disclosure (details / summary)

Regions that open and close are elements too.

Dialog

<dialog> and the backdrop behind it are both styled.

Disclosure without JavaScript

A button with popovertarget opens and closes a [popover] panel with no script at all — the browser wires the click. What it does not do is keep the button’s aria-expanded in step; only <details> gets that for free. The marker is drawn purely from that attribute, the same way aria-sort’s is: borders, not a glyph, so it needs no font and no fallback. This page keeps it in sync with a few lines of its own script — the CSS itself never assumes those lines exist.

This panel is a [popover]. It shares its glass surface with <dialog> instead of repeating it — see the CSS for what that rule leaves out on purpose.

A browser that does not know [popover] yet shows this panel as a plain block, permanently open. That is the accepted cost of styling an attribute this new — nothing here reaches for :popover-open, so the declared browser target does not move.

role="tablist" — Tabs

The selected tab is drawn with the same fill as aria-pressed — role="tab" just carries aria-selected instead. What differs is behaviour, not looks: a tab's arrow keys move the selection with them, panel swap included (WAI-ARIA APG's "automatic activation"), which a toggle button never does.

The selected panel's own content. role="tabpanel" carries tabindex="0", so a keyboard user reads it as one stop rather than tabbing through everything inside it.

The look is CSS alone, but moving the selection with the arrow keys and swapping panels is @ryo9ra/su-css/behaviors.js's job. Without it, this is just a row of buttons whose selection never leaves the first one.

Things you cannot operate

disabled is a form-control attribute, and a link cannot carry one. aria-disabled="true" says the same thing about anything at all, and SuCSS fades it to the same strength.

Next step

Neither one is faded out of the tab order: Tab still lands on both, so a screen reader can reach them and say why they are out. SuCSS adds no pointer-events: none — stopping the click is the handler's job, and CSS cannot give back what it takes here.

This step cannot be started yet

inert on a region takes everything inside it out of focus, clicks and selection. The browser does all of that and shows none of it, so the fading is the stylesheet's half of the job.

Tab skips the panel above entirely — that half is the browser's.

Things still loading

aria-busy="true" says an element’s contents are not settled yet. Assistive technology already reads it, so the page can say the same thing on screen without a class. A button gets the indicator before its label; anything larger gets one in its corner, out of the flow, where it cannot push around the content it is annotating.

Recent deploys

Fetching the last ten runs…

Busy is not disabled. The button above still takes a press, because aria-busy says the answer has not arrived, not that the control is out — add disabled as well if the press should be refused. Under prefers-reduced-motion the ring stops turning and stays put, so the state keeps a shape once the movement is gone.

Messages worth stopping for

role="status" above is a badge because a status is a word or two. A sentence takes a block instead: role="alert" for something wrong, role="note" for something worth knowing.

The last deploy did not finish. Nothing was published, and the previous version is still live.

Changing --hue moves the background gradient too, not just the accent.

Those are the only two, because they are the only two severities with a role to stand on. "Succeeded" and "be careful" would need a class to say which they are, and this framework does not have one.

Code and media

Code blocks scroll sideways rather than pushing the page. <figure> with a <figcaption> is handled too.

<article>
  <h2>No classes needed</h2>
  <p>Write the element and it is styled.</p>
</article>
The SuCSS cover artwork
The brand assets live in assets/brand/.

Customizing

Colours, radii, spacing and type are all custom properties. A few lines in :root move the whole page — or open the customizer and drag the controls until it looks right.

:root {
  --hue: 158;          /* accent colour and background mesh */
  --sat: 70%;
  --radius: 0.5rem;    /* calmer corners */
  --max-width: 72ch;   /* width of the body column */
  --base-size: 1rem;
  --font-sans: "Inter", sans-serif;
}

Pinning a theme

By default the theme follows the operating system. data-theme on <html> pins it — which is all the buttons at the top of this page do.

<html data-theme="dark">   <!-- always dark -->
<html data-theme="light">  <!-- always light -->
<html>                     <!-- follow the OS (default) -->