CSS mixins without Sass: Three practical ways with pure CSS

In CSS, you can achieve mixin-like patterns by combining classes and custom properties, even without using Sass. In this tutorial, you will learn three simple patterns that I use in real projects. You will also get a complete HTML file you can paste into your editor and test.

What is a CSS Mixin in simple words?

A mixin is a reusable set of styles you can apply in many places. In Sass, you write a mixin and include it. In plain CSS, you can get the same feel by composing classes, by using CSS variables, and by sharing rules across components.

Pattern 1: Composition classes, mixin in your HTML

Create small, reusable classes, then stack them on an element. For example, a class that makes things look pressable, and a class that makes corners round. You apply both to the element. This is simple and very fast to understand.

  • Example class in CSS: .pressable { box-shadow: 0 2px 6px rgba(0,0,0,.2); }
  • Example HTML using two mixin-like classes: <a class="pressable rounded">Pill link</a>

When you add two classes, like class="pressable rounded", the element gets the styles from both. I like this because I can reuse the same building blocks on buttons, links, and tags.

Pattern 2: CSS custom properties as mixin knobs

A common wish is to make the same class look different in different places. Do this with CSS custom properties, also called CSS variables. You define your class once, then override the variables where needed.

  • The key line is background: var(--bg, #4f46e5). The value after the comma is the fallback. If --bg is not set, the background uses #4f46e5.
  • To change color in one place, you can write inline: style="--bg:#059669". You can also make a variant class, for example .is-secondary { --bg:#e5e7eb }.

Inside the reusable class, write all the properties with variables, like color: var(--fg, #fff), padding: var(--pad, .6rem 1rem), and border-radius: var(--radius, .5rem). Now any element can override these tokens without touching the base class. This gives you a mixin feel with one class and some knobs.

Tip from my practice: name variables by intent, not by color name. I use --bg and --fg, not --green, because the same component may change theme later.

Pattern 3: Share base rules with a selector list

Sometimes you want a base set of styles shared by different components, for example, buttons and chips. You can write one rule that targets both, then add small differences per component.

  • The key rule looks like :where(.btn, .chip) { display:inline-flex; gap:.5rem; }
  • I use :where(...) because it has zero specificity. This makes later overrides easier. If you write .btn, .chip { ... } it also works; just be mindful of specificity when you override.

Then add component specifics, for example .chip { background:#f3f4f6 } and .btn--accent { background:#10b981 }. This pattern keeps code dry without any build tools.

Things to avoid and remember

  • You cannot store multiple CSS declarations inside one variable. A line like --card: { padding:1rem; border:1px solid } will not work. Define variables for values, then write the properties once in a base class.
  • The old @apply idea is not supported in browsers. Stick to classes, variables, and shared rules.
  • Keep specificity low. Using :where(...) or single class selectors helps you override styles cleanly.

Try it now: complete HTML you can paste and run

Copy this into a file named index.html, open it in a browser, then resize the window to see it on mobile.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS Mixins without Sass - Demos</title>
<style>
:root {
color-scheme: light dark;
--bg-page: #ffffff;
--fg-page: #111827;
--muted: #6b7280;
--card: #f9fafb;
--border: #e5e7eb;
--brand: #4f46e5;
--success: #10b981;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-page: #0b1020;
--fg-page: #e5e7eb;
--muted: #9ca3af;
--card: #0f172a;
--border: #243041;
}
}

* { box-sizing: border-box; }
body {
margin: 0;
font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg-page);
color: var(--fg-page);
}
.wrap {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem;
display: grid;
gap: 2rem;
}
h1, h2 { line-height: 1.2; margin: 0 0 .5rem; }
p { margin: 0 0 1rem; color: var(--muted); }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: .75rem;
padding: 1rem;
}
.row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
@media (max-width: 720px) {
.row { grid-template-columns: 1fr; }
}
.demo h2 { margin-bottom: .25rem; }
.demo small { color: var(--muted); }

/* Demo 1: composition classes */
.demo1 .pressable {
background: var(--brand);
color: #fff;
border: none;
padding: .6rem 1rem;
border-radius: .5rem;
box-shadow: 0 2px 6px rgba(0,0,0,.2);
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: transform .12s ease, box-shadow .12s ease;
}
.demo1 .pressable:hover {
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(0,0,0,.25);
}
.demo1 .rounded { border-radius: 999px; }
.demo1 .outline {
background: transparent;
color: var(--brand);
border: 2px solid currentColor;
box-shadow: none;
}

/* Demo 2: custom properties as knobs */
.demo2 .pressable {
background: var(--bg, var(--brand));
color: var(--fg, #fff);
padding: var(--pad, .6rem 1rem);
border-radius: var(--radius, .5rem);
border: var(--borderLine, none);
box-shadow: var(--shadow, 0 2px 6px rgba(0,0,0,.2));
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: transform .12s ease, box-shadow .12s ease;
}
.demo2 .pressable:hover {
transform: translateY(-1px);
box-shadow: var(--shadowHover, 0 8px 20px rgba(0,0,0,.25));
}
.demo2 .is-secondary {
--bg: #e5e7eb;
--fg: #111827;
--shadow: 0 2px 6px rgba(0,0,0,.1);
--shadowHover: 0 8px 16px rgba(0,0,0,.15);
}
.demo2 .is-ghost {
--bg: transparent;
--fg: var(--brand);
--borderLine: 2px solid currentColor;
--shadow: none;
--shadowHover: none;
}

/* Demo 3: shared base rules */
.demo3 :where(.btn, .chip) {
display: inline-flex;
align-items: center;
gap: .5rem;
font: 600 1rem/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
padding: .5rem .9rem;
border-radius: .6rem;
user-select: none;
}
.demo3 .btn {
background: #111827;
color: #fff;
border: none;
cursor: pointer;
}
.demo3 .btn--accent { background: var(--success); }
.demo3 .chip {
background: #f3f4f6;
color: #111827;
border: 1px solid #e5e7eb;
}
.demo3 .chip .dot {
width: .5rem; height: .5rem; border-radius: 999px; background: var(--success);
}
</style>
</head>
<body>
<main class="wrap">
<header>
<h1>CSS mixins without Sass</h1>
<p>Three patterns you can use today, no build step required.</p>
</header>

<section class="demo demo1 card">
<h2>1) Composition classes</h2>
<small>Combine small classes on your HTML elements</small>
<div class="row" style="margin-top:.75rem">
<div>
<button class="pressable">Primary button</button>
</div>
<div>
<a href="#" class="pressable rounded">Pill link</a>
</div>
<div>
<button class="pressable outline">Outline button</button>
</div>
</div>
</section>

<section class="demo demo2 card">
<h2>2) Custom properties as knobs</h2>
<small>Override tokens per element or with variant classes</small>
<div class="row" style="margin-top:.75rem">
<div>
<button class="pressable">Default tokens</button>
</div>
<div>
<button class="pressable is-secondary">Secondary via class</button>
</div>
<div>
<button class="pressable is-ghost" style="--radius: 999px; --pad: .5rem 1.25rem;">
Ghost with overrides
</button>
</div>
</div>
</section>

<section class="demo demo3 card">
<h2>3) Shared base with selector list</h2>
<small>Share core rules, then customize each component</small>
<div class="row" style="margin-top:.75rem">
<div>
<button class="btn">Default button</button>
</div>
<div>
<button class="btn btn--accent">Accent button</button>
</div>
<div>
<span class="chip"><span class="dot" aria-hidden="true"></span>New</span>
</div>
</div>
</section>
</main>
</body>
</html>

Output:

Image showing CSS mixins examples without Saas.

Why these work

  • Composition classes give you reusable chunks. You mix them in with the class attribute.
  • Custom properties give you parameters. You keep one base class, and you change values with --var overrides.
  • Selector lists let you share a base across different components. You add small differences later.

I suggest you start with pattern 2 for most UI parts. It keeps HTML clean, and it is easy to theme.

See also:

Vinish Kapoor
Vinish Kapoor

Vinish Kapoor is a seasoned software development professional and a fervent enthusiast of artificial intelligence (AI). His impressive career spans over 25+ years, marked by a relentless pursuit of innovation and excellence in the field of information technology. As an Oracle ACE, Vinish has distinguished himself as a leading expert in Oracle technologies, a title awarded to individuals who have demonstrated their deep commitment, leadership, and expertise in the Oracle community.

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments