Technology Apr 29, 2026 · 20 min read

Style HTML without writing a single class — introducing classless.css

I had a weird thought while building a docs page last month. I was writing this: <button class="btn btn-primary">Get started</button> And I thought — why does a button need to be told it's a button? It is a button. The browser knows it. The screen reader knows it. Why doesn't t...

DE
DEV Community
by nJ
Style HTML without writing a single class — introducing classless.css

I had a weird thought while building a docs page last month.

I was writing this:

<button class="btn btn-primary">Get started</button>

And I thought — why does a button need to be told it's a button? It is a button. The browser knows it. The screen reader knows it. Why doesn't the CSS?

That thought turned into classless.css — a 47 KB stylesheet that makes plain, semantic HTML look great without adding a single class to your markup.

What does "classless" mean?

The idea is simple: instead of styling elements by class names, you style native HTML elements directly.

Normal CSS library:

<button class="btn btn-primary btn-lg">Submit</button>
<input class="input input-bordered" type="text">
<div class="card card-body shadow">Content</div>

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>

One <link> tag — and your HTML looks good. No class names to remember, no documentation to check for every element.

Who is this actually for?

Before I explain how it works, let me be honest about the use cases — because classless CSS is not for everything.

Perfect for:

  • Markdown-rendered content (blogs, docs, READMEs rendered as HTML)
  • Quick prototypes where you want something decent without thinking about classes
  • Email templates and static pages with semantic HTML
  • Dropping into an existing project to style a section without touching the markup
  • Developers who just want to write HTML and move on

Not ideal for:

  • Complex UI with many component variants
  • Projects where you need full control over every detail
  • Apps with existing CSS that would conflict

How it works

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">

That's it. Now write plain HTML:

<main>
  <h1>Hello World</h1>
  <p>This paragraph is styled automatically.</p>

  <button>Get started</button>
  <button data-variant="accent">Secondary</button>

  <input type="email" placeholder="your@email.com">
  <textarea placeholder="Your message..."></textarea>

  <table>
    <thead><tr><th>Name</th><th>Role</th></tr></thead>
    <tbody>
      <tr><td>Alice</td><td>Designer</td></tr>
      <tr><td>Bob</td><td>Developer</td></tr>
    </tbody>
  </table>
</main>

Every element — headings, paragraphs, buttons, inputs, tables, lists, code blocks, blockquotes — gets thoughtful styles automatically.

Try it live:

Variants without classes

The one concession to "no classes" is variants. Instead of class names, classless.css uses data-* attributes:

<!-- Button variants -->
<button>Default</button>
<button data-variant="accent">Accent</button>
<button data-variant="success">Success</button>
<button data-variant="danger">Danger</button>
<button data-variant="ghost">Ghost</button>

<!-- Input states -->
<input type="text" data-variant="success" value="Valid input">
<input type="text" data-variant="danger" placeholder="Error state">

<!-- Alert boxes -->
<div role="note">Neutral info</div>
<div role="note" data-variant="success">Operation complete</div>
<div role="note" data-variant="warning">Check this carefully</div>
<div role="note" data-variant="danger">Something went wrong</div>

data-variant keeps markup readable and semantic — you're describing what the element is, not what it should look like.

9 themes — same as the full library

classless.css shares the same theming system as the full njX UI library. Set data-theme on <html> and everything updates:

<html data-theme="dark">    <!-- default -->
<html data-theme="light">
<html data-theme="purple">
<html data-theme="cyan">
<!-- + red, blue, green, yellow, pink -->

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')

This works because both files use the same CSS custom properties from _base.css. The tokens are identical — only the scoping is different.

Scoping — it doesn't conflict with your existing CSS

Here's one thing I'm proud of: classless.css is fully scoped. Styles only activate inside elements that have no class:

:where(main:not([class]), article:not([class]), section:not([class]), form:not([class])) {
  /* all classless styles live here */
}

This means you can use classless.css alongside style.min.css (the full library) or even Bootstrap — it will only apply where you have unclassed containers. No conflicts.

<!-- This gets classless styles -->
<main>
  <h1>Styled automatically</h1>
  <button>Looks great</button>
</main>

<!-- This is untouched -->
<div class="my-custom-section">
  <button class="btn btn-primary">Full library button</button>
</div>

Loading states — no extra markup

One of my favorite parts: loading states are built in.

<!-- Busy container — overlay spinner appears automatically -->
<article aria-busy="true">
  Loading content...
</article>

<!-- Loading button — spinner replaces text -->
<button aria-busy="true">Saving...</button>

<!-- Inline spinner -->
<span data-loading></span> Fetching data...

No JavaScript. No extra elements. Just semantic HTML attributes.

Details and dialog — CSS-only interactive

<!-- Accordion — native HTML, styled automatically -->
<details>
  <summary>Click to expand</summary>
  <p>Hidden content revealed with smooth animation.</p>
</details>

<!-- Dialog — open with JS, styled automatically -->
<dialog id="my-dialog">
  <h2>Confirm action</h2>
  <p>Are you sure?</p>
  <button onclick="document.getElementById('my-dialog').close()">Close</button>
</dialog>
<button onclick="document.getElementById('my-dialog').showModal()">Open dialog</button>

Compared to PicoCSS

The closest thing to classless.css is PicoCSS, which I love and respect. The key differences:

PicoCSS njX classless.css
Themes 2 (light/dark) 9
Variant system class-based data-attribute
Loading states ✅ aria-busy + data-loading
Scoped (no conflicts)
Works with full library
Size ~10 KB 47 KB
Tooltip ✅ data-tooltip

PicoCSS is smaller and more minimal. classless.css trades size for more features and better theming.

Try it

<!DOCTYPE html>
<html data-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My page</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">
</head>
<body>
<main>
  <h1>Hello World</h1>
  <p>Write semantic HTML. Get beautiful styles.</p>

  <button>Default button</button>
  <button data-variant="accent">Accent</button>

  <input type="email" placeholder="your@email.com">

  <details>
    <summary>Learn more</summary>
    <p>No classes needed. Just HTML.</p>
  </details>
</main>
</body>
</html>

I had a weird thought while building a docs page last month.

I was writing this:

<button class="btn btn-primary">Get started</button>

And I thought — why does a button need to be told it's a button? It is a button. The browser knows it. The screen reader knows it. Why doesn't the CSS?

That thought turned into classless.css — a 47 KB stylesheet that makes plain, semantic HTML look great without adding a single class to your markup.

What does "classless" mean?

The idea is simple: instead of styling elements by class names, you style native HTML elements directly.

Normal CSS library:

<button class="btn btn-primary btn-lg">Submit</button>
<input class="input input-bordered" type="text">
<div class="card card-body shadow">Content</div>

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>

One <link> tag — and your HTML looks good. No class names to remember, no documentation to check for every element.

Who is this actually for?

Before I explain how it works, let me be honest about the use cases — because classless CSS is not for everything.

Perfect for:

  • Markdown-rendered content (blogs, docs, READMEs rendered as HTML)
  • Quick prototypes where you want something decent without thinking about classes
  • Email templates and static pages with semantic HTML
  • Dropping into an existing project to style a section without touching the markup
  • Developers who just want to write HTML and move on

Not ideal for:

  • Complex UI with many component variants
  • Projects where you need full control over every detail
  • Apps with existing CSS that would conflict

How it works

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">

That's it. Now write plain HTML:

<main>
  <h1>Hello World</h1>
  <p>This paragraph is styled automatically.</p>

  <button>Get started</button>
  <button data-variant="accent">Secondary</button>

  <input type="email" placeholder="your@email.com">
  <textarea placeholder="Your message..."></textarea>

  <table>
    <thead><tr><th>Name</th><th>Role</th></tr></thead>
    <tbody>
      <tr><td>Alice</td><td>Designer</td></tr>
      <tr><td>Bob</td><td>Developer</td></tr>
    </tbody>
  </table>
</main>

Every element — headings, paragraphs, buttons, inputs, tables, lists, code blocks, blockquotes — gets thoughtful styles automatically.

Try it live:

Variants without classes

The one concession to "no classes" is variants. Instead of class names, classless.css uses data-* attributes:

<!-- Button variants -->
<button>Default</button>
<button data-variant="accent">Accent</button>
<button data-variant="success">Success</button>
<button data-variant="danger">Danger</button>
<button data-variant="ghost">Ghost</button>

<!-- Input states -->
<input type="text" data-variant="success" value="Valid input">
<input type="text" data-variant="danger" placeholder="Error state">

<!-- Alert boxes -->
<div role="note">Neutral info</div>
<div role="note" data-variant="success">Operation complete</div>
<div role="note" data-variant="warning">Check this carefully</div>
<div role="note" data-variant="danger">Something went wrong</div>

data-variant keeps markup readable and semantic — you're describing what the element is, not what it should look like.

9 themes — same as the full library

classless.css shares the same theming system as the full njX UI library. Set data-theme on <html> and everything updates:

<html data-theme="dark">    <!-- default -->
<html data-theme="light">
<html data-theme="purple">
<html data-theme="cyan">
<!-- + red, blue, green, yellow, pink -->

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')

This works because both files use the same CSS custom properties from _base.css. The tokens are identical — only the scoping is different.

Scoping — it doesn't conflict with your existing CSS

Here's one thing I'm proud of: classless.css is fully scoped. Styles only activate inside elements that have no class:

:where(main:not([class]), article:not([class]), section:not([class]), form:not([class])) {
  /* all classless styles live here */
}

This means you can use classless.css alongside style.min.css (the full library) or even Bootstrap — it will only apply where you have unclassed containers. No conflicts.

<!-- This gets classless styles -->
<main>
  <h1>Styled automatically</h1>
  <button>Looks great</button>
</main>

<!-- This is untouched -->
<div class="my-custom-section">
  <button class="btn btn-primary">Full library button</button>
</div>

Loading states — no extra markup

One of my favorite parts: loading states are built in.

<!-- Busy container — overlay spinner appears automatically -->
<article aria-busy="true">
  Loading content...
</article>

<!-- Loading button — spinner replaces text -->
<button aria-busy="true">Saving...</button>

<!-- Inline spinner -->
<span data-loading></span> Fetching data...

No JavaScript. No extra elements. Just semantic HTML attributes.

Details and dialog — CSS-only interactive

<!-- Accordion — native HTML, styled automatically -->
<details>
  <summary>Click to expand</summary>
  <p>Hidden content revealed with smooth animation.</p>
</details>

<!-- Dialog — open with JS, styled automatically -->
<dialog id="my-dialog">
  <h2>Confirm action</h2>
  <p>Are you sure?</p>
  <button onclick="document.getElementById('my-dialog').close()">Close</button>
</dialog>
<button onclick="document.getElementById('my-dialog').showModal()">Open dialog</button>

Compared to PicoCSS

The closest thing to classless.css is PicoCSS, which I love and respect. The key differences:

PicoCSS njX classless.css
Themes 2 (light/dark) 9
Variant system class-based data-attribute
Loading states ✅ aria-busy + data-loading
Scoped (no conflicts)
Works with full library
Size ~10 KB 47 KB
Tooltip ✅ data-tooltip

PicoCSS is smaller and more minimal. classless.css trades size for more features and better theming.

Try it

<!DOCTYPE html>
<html data-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My page</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">
</head>
<body>
<main>
  <h1>Hello World</h1>
  <p>Write semantic HTML. Get beautiful styles.</p>

  <button>Default button</button>
  <button data-variant="accent">Accent</button>

  <input type="email" placeholder="your@email.com">

  <details>
    <summary>Learn more</summary>
    <p>No classes needed. Just HTML.</p>
  </details>
</main>
</body>
</html>

I had a weird thought while building a docs page last month.

I was writing this:

<button class="btn btn-primary">Get started</button>

And I thought — why does a button need to be told it's a button? It is a button. The browser knows it. The screen reader knows it. Why doesn't the CSS?

That thought turned into classless.css — a 47 KB stylesheet that makes plain, semantic HTML look great without adding a single class to your markup.

What does "classless" mean?

The idea is simple: instead of styling elements by class names, you style native HTML elements directly.

Normal CSS library:

<button class="btn btn-primary btn-lg">Submit</button>
<input class="input input-bordered" type="text">
<div class="card card-body shadow">Content</div>

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>

One <link> tag — and your HTML looks good. No class names to remember, no documentation to check for every element.

Who is this actually for?

Before I explain how it works, let me be honest about the use cases — because classless CSS is not for everything.

Perfect for:

  • Markdown-rendered content (blogs, docs, READMEs rendered as HTML)
  • Quick prototypes where you want something decent without thinking about classes
  • Email templates and static pages with semantic HTML
  • Dropping into an existing project to style a section without touching the markup
  • Developers who just want to write HTML and move on

Not ideal for:

  • Complex UI with many component variants
  • Projects where you need full control over every detail
  • Apps with existing CSS that would conflict

How it works

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">

That's it. Now write plain HTML:

<main>
  <h1>Hello World</h1>
  <p>This paragraph is styled automatically.</p>

  <button>Get started</button>
  <button data-variant="accent">Secondary</button>

  <input type="email" placeholder="your@email.com">
  <textarea placeholder="Your message..."></textarea>

  <table>
    <thead><tr><th>Name</th><th>Role</th></tr></thead>
    <tbody>
      <tr><td>Alice</td><td>Designer</td></tr>
      <tr><td>Bob</td><td>Developer</td></tr>
    </tbody>
  </table>
</main>

Every element — headings, paragraphs, buttons, inputs, tables, lists, code blocks, blockquotes — gets thoughtful styles automatically.

Try it live:

Variants without classes

The one concession to "no classes" is variants. Instead of class names, classless.css uses data-* attributes:

<!-- Button variants -->
<button>Default</button>
<button data-variant="accent">Accent</button>
<button data-variant="success">Success</button>
<button data-variant="danger">Danger</button>
<button data-variant="ghost">Ghost</button>

<!-- Input states -->
<input type="text" data-variant="success" value="Valid input">
<input type="text" data-variant="danger" placeholder="Error state">

<!-- Alert boxes -->
<div role="note">Neutral info</div>
<div role="note" data-variant="success">Operation complete</div>
<div role="note" data-variant="warning">Check this carefully</div>
<div role="note" data-variant="danger">Something went wrong</div>

data-variant keeps markup readable and semantic — you're describing what the element is, not what it should look like.

9 themes — same as the full library

classless.css shares the same theming system as the full njX UI library. Set data-theme on <html> and everything updates:

<html data-theme="dark">    <!-- default -->
<html data-theme="light">
<html data-theme="purple">
<html data-theme="cyan">
<!-- + red, blue, green, yellow, pink -->

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')

This works because both files use the same CSS custom properties from _base.css. The tokens are identical — only the scoping is different.

https://codepen.io/njbSaab/pen/vEyBvoN

Scoping — it doesn't conflict with your existing CSS

Here's one thing I'm proud of: classless.css is fully scoped. Styles only activate inside elements that have no class:

:where(main:not([class]), article:not([class]), section:not([class]), form:not([class])) {
  /* all classless styles live here */
}

This means you can use classless.css alongside style.min.css (the full library) or even Bootstrap — it will only apply where you have unclassed containers. No conflicts.

<!-- This gets classless styles -->
<main>
  <h1>Styled automatically</h1>
  <button>Looks great</button>
</main>

<!-- This is untouched -->
<div class="my-custom-section">
  <button class="btn btn-primary">Full library button</button>
</div>

Loading states — no extra markup

One of my favorite parts: loading states are built in.

<!-- Busy container — overlay spinner appears automatically -->
<article aria-busy="true">
  Loading content...
</article>

<!-- Loading button — spinner replaces text -->
<button aria-busy="true">Saving...</button>

<!-- Inline spinner -->
<span data-loading></span> Fetching data...

No JavaScript. No extra elements. Just semantic HTML attributes.

Details and dialog — CSS-only interactive

<!-- Accordion — native HTML, styled automatically -->
<details>
  <summary>Click to expand</summary>
  <p>Hidden content revealed with smooth animation.</p>
</details>

<!-- Dialog — open with JS, styled automatically -->
<dialog id="my-dialog">
  <h2>Confirm action</h2>
  <p>Are you sure?</p>
  <button onclick="document.getElementById('my-dialog').close()">Close</button>
</dialog>
<button onclick="document.getElementById('my-dialog').showModal()">Open dialog</button>

Compared to PicoCSS

The closest thing to classless.css is PicoCSS, which I love and respect. The key differences:

PicoCSS njX classless.css
Themes 2 (light/dark) 9
Variant system class-based data-attribute
Loading states ✅ aria-busy + data-loading
Scoped (no conflicts)
Works with full library
Size ~10 KB 47 KB
Tooltip ✅ data-tooltip

PicoCSS is smaller and more minimal. classless.css trades size for more features and better theming.

Try it

<!DOCTYPE html>
<html data-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My page</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui/css/classless.min.css">
</head>
<body>
<main>
  <h1>Hello World</h1>
  <p>Write semantic HTML. Get beautiful styles.</p>

  <button>Default button</button>
  <button data-variant="accent">Accent</button>

  <input type="email" placeholder="your@email.com">

  <details>
    <summary>Learn more</summary>
    <p>No classes needed. Just HTML.</p>
  </details>
</main>
</body>
</html>

Live docs: njxui.dev
Full classless reference: classless.md
npm: npm i njx-uicss/classless.min.css

I've been using classless mode for quick internal tools and prototypes where I don't want to think about class names. It's genuinely refreshing to just write HTML and have it look decent.

What do you think — is classless CSS a useful pattern or just a fun experiment? Would love to hear how you handle styling for quick projects.

Live docs: njxui.dev
Full classless reference: classless.md
npm: npm i njx-uicss/classless.min.css

I've been using classless mode for quick internal tools and prototypes where I don't want to think about class names. It's genuinely refreshing to just write HTML and have it look decent.

What do you think — is classless CSS a useful pattern or just a fun experiment? Would love to hear how you handle styling for quick projects.

Live docs: njxui.dev
Full classless reference: classless.md
npm: npm i njx-uicss/classless.min.css

I've been using classless mode for quick internal tools and prototypes where I don't want to think about class names. It's genuinely refreshing to just write HTML and have it look decent.

What do you think — is classless CSS a useful pattern or just a fun experiment? Would love to hear how you handle styling for quick projects.

DE
Source

This article was originally published by DEV Community and written by nJ.

Read original article on DEV Community
Back to Discover

Reading List