DEV Community

Cover image for Style HTML without writing a single class — introducing classless.css
nJ
nJ

Posted on

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

I was writing this:

<button class="btn btn-primary">Get started</button>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

I was writing this:

<button class="btn btn-primary">Get started</button>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Classless approach:

<button>Submit</button>
<input type="text">
<article>Content</article>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

Switch at runtime:

document.documentElement.setAttribute('data-theme', 'purple')
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)