No class names
Styling comes from the elements themselves. There is no naming convention to agree on and no utility classes to memorise.
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.
Styling comes from the elements themselves. There is no naming convention to agree on and no utility classes to memorise.
Follows prefers-color-scheme, and data-theme pins it either way.
WCAG AA/AAA contrast, visible :focus-visible rings, adequate touch targets. It honours prefers-reduced-motion and prefers-reduced-transparency too.
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.
A single stylesheet, 6.6 KB minified and gzipped. No JavaScript, no build step.
Every colour, radius and rhythm is a custom property. Change --hue and the whole page moves — try it in the customizer.
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';
| 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.
Everything below is plain HTML with no class attribute. Open the page source and compare.
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
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>| 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.
|
||
| 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.
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
Regions that open and close are elements too.
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.
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 second panel. Arrowing here flips both the CSS state and this panel's visibility at once.
The third panel. Home and End jump to the first and last tab.
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.
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.
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.
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.
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.
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.
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 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>
assets/brand/.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;
}
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) -->