A clean, readable, and pragmatic naming convention methodology for CSS classes that balances structure with developer autonomy.
Finally, a CSS methodology that feels intuitive, scales naturally, and gives developers the structure they needβwithout the overhead they dread.
Fluffy CSS is ideal for individual developers and teams building modern web apps with component-based frameworks like Vue or React. It's designed to scale smoothly from small side projects to complex interfaces while keeping naming conventions simple and maintainable.
CSS methodologies like BEM or SMACSS are powerful but often feel verbose, rigid, or difficult to onboard. Fluffy CSS aims to offer:
- Readable and aesthetically pleasing class names
- Consistency across components without excessive boilerplate
- Natural compatibility with component-based frameworks like Vue and React
- Enough flexibility to avoid "methodology police" while keeping code structured and maintainable
-
Class Names
- Use only lowercase letters and dashes (
-
). - Class names must start with a letter.
- Dashes must be surrounded by letters (e.g.
foo-bar
, notfoo--bar
or-active
).
- Use only lowercase letters and dashes (
-
Modifiers
- Begin with
is-
orhas-
(e.g.is-disabled
,has-footer
). - Must be used alongside their base class (never standalone).
- Begin with
-
Parent-Child Naming
- Prefix child elements with their parent block name (e.g.
profile-header-title
). - If the child is conceptually independent or deeply nested, introduce a new name. A helpful signal for this is when the class name becomes excessively long β e.g.
app-input-control-right-content-icon
suggests a structure that's too deeply nested and tightly coupled. In such cases, extract a new conceptual name likeright-content-icon
to represent the subcomponent or utility clearly and cleanly.
- Prefix child elements with their parent block name (e.g.
-
Global Classes
- Use only single-word global classes (e.g.
button
). - Modifiers may be added to global classes and must follow modifier naming conventions (e.g.
button is-secondary
).
- Use only single-word global classes (e.g.
-
Class Name Order
- The order of classes matters for clarity and consistency.
- Always place the element-specific class first, then global classes and their modifiers.
- Example:
modal-content-close-button button is-icon
is preferred overbutton modal-content-close-button is-icon
.
-
Selectors
- Use only class and pseudo-class selectors. Modifier selectors (e.g.
.is-active
) must never be used independently. Always scope them relative to their base class using SCSS nesting (e.g.&.is-active { ... }
). This enforces a structural connection between the modifier and its parent component. - Avoid tag-based selectors except in controlled slots (e.g.
strong
inside content). For example, if your component has a slot for rich text, and you expect semantic tags like<strong>
, itβs fine to style thestrong
tag directly without requiring a class. This avoids unnecessary markup and ensures consistent styling.
- Use only class and pseudo-class selectors. Modifier selectors (e.g.
These are highly recommended rules that influence how you should apply Fluffy CSS in real-world development:
- Avoid deep nesting in structure; refactor into smaller components if needed.
- Use SCSS nesting cautiously; avoid selectors deeper than 3 levels.
- Avoid global utility classes (e.g.
u-hidden
,text-ellipsis
). Instead, use SCSS mixins for common visual behaviors or patterns like text styles or media queries. This reduces cognitive load β you donβt need to remember global class names or wonder how they affect other parts of your app. Mixins apply styles directly in your local context, making class name clashing impossible and your components more predictable.
.product-card-description {
@include utils.ellipsis;
}
.login-page-title {
@include utils.text-h1;
}
These mixins offer the benefits of reusable logic without polluting global class space or breaking component encapsulation.
These aren't strictly required by the methodology, but they help maintain consistency and developer happiness:
- Name components with at least two words (e.g.
SignUpForm
,LoginPage
). - The root class of the component should match the component name. For example, a
DashboardPage
component should have a top-level container with the classdashboard-page
. - Prefix library components with
App
(e.g.AppInput
,AppModal
).
<!-- β Wrong -->
<span class="is-error">Required</span>
is-error
is a modifier and should never appear without a base class.
<!-- β
Correct -->
<span class="form-error-message is-error">Required</span>
// β Too much nesting
.account-widget {
.account-widget-header {
.account-widget-header-logo {
.account-widget-header-logo-icon {
color: var(--color-success);
}
}
.account-widget-header-title {
strong {
//...
}
}
}
}
Deep nesting increases specificity and coupling. Split into several blocks.
// β
Better structure
.account-widget {
//...
}
.account-widget-header {
//...
.account-widget-header-logo {
//...
}
// No need in nesting inside `.account-widget-header-logo`,
// plain structure is better here
.account-widget-header-logo-icon {
//...
}
.account-widget-header-title {
//...
strong {
//...
}
}
}
<!-- β Wrong -->
<button class="is-primary is-rounded">Click me</button>
Modifiers should extend or describe a base class. Without one, they're meaningless.
<!-- β
Correct -->
<button class="action-button is-primary is-rounded">Click me</button>
// β Avoid this
button {
color: var(--color-error);
}
Tag selectors should only be used in very specific, controlled slots (e.g. content with known HTML).
// β
Prefer class-based selectors
.app-form-cancel-button {
color: var(--color-error);
}
<!-- β Avoid cryptic names -->
<div class="prfl-hdr-ttl"></div>
Class names should be self-explanatory. Prefer clarity over brevity.
<!-- β
Clear and semantic -->
<div class="profile-header-title"></div>
This section showcases how to apply Fluffy CSS in real-world components, ranging from simple building blocks to more complex layouts. The examples are written in Vue for clarity and structure, but the naming conventions and methodology principles easily translate to React, Svelte, or any other component-based framework. Use these as inspiration or references when adopting Fluffy CSS in your own projects.
<template>
<div class="profile-badge">
<img
class="profile-badge-avatar"
:src="avatarUrl"
/>
<span class="profile-badge-name">{{ fullName }}</span>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/utils' as utils;
.profile-badge {
//...
.profile-badge-avatar {
//...
}
.profile-badge-name {
//...
@include utils.text-caption;
}
}
</style>
<template>
<section class="settings-section">
<h2 class="settings-section-title">Notifications</h2>
<p class="settings-section-description">Manage your notification preferences</p>
<AppCheckbox
class="settings-notification-checkbox"
label="Email Alerts"
:is-checked="isEmailEnabled"
/>
</section>
</template>
<style scoped lang="scss">
@use '@/styles/utils' as utils;
.settings-section {
//...
.settings-section-title {
//...
@include utils.text-h2;
}
.settings-section-description {
//...
@include utils.text-description;
}
.settings-notification-checkbox {
//...
}
}
</style>
<template>
<div class="dashboard-page">
<div class="dashboard-page-header">
<AppIcon
class="dashboard-page-header-icon"
:name="AppIconName.STATUS"
/>
<h1 class="dashboard-page-header-title">Dashboard</h1>
<p class="dashboard-page-header-description">Displays common statistics</p>
</div>
<div class="dashboard-page-content">
<DashboardPageStatistics class="dashboard-page-content-statistics" />
<DashboardPageBalances class="dashboard-page-content-balances" />
</div>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/utils' as utils;
.dashboard-page {
//...
}
.dashboard-page-header {
//...
.dashboard-page-header-icon {
//...
}
.dashboard-page-header-title {
//...
@include utils.text-h1;
}
.dashboard-page-header-description {
//...
@include utils.text-description;
}
}
.dashboard-page-content {
//...
}
</style>
<template>
<div
class="product-card"
:class="{ 'has-footer': hasFooter }"
>
<img
class="product-card-image"
:src="image"
/>
<div class="product-card-info">
<h3 class="product-card-info-title is-sale">{{ title }}</h3>
<p class="product-card-info-description">{{ description }}</p>
<ProductCardPrice
class="product-card-info-price"
:price="price"
/>
</div>
<div
v-if="hasFooter"
class="product-card-footer"
>
<ProductCardSaleBadge :type="saleType" />
</div>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/utils' as utils;
.product-card {
//...
&.has-footer {
//...
}
.product-card-image {
//...
}
}
.product-card-info {
//...
.product-card-info-title {
//...
@include utils.text-h3;
&.is-sale {
//...
}
}
.product-card-info-description {
//...
@include utils.ellipsis;
}
.product-card-info-price {
//...
}
}
.product-card-footer {
//...
}
</style>
<template>
<label
class="app-switch"
:class="{ 'is-disabled': isDisabled }"
>
<input
class="app-switch-input"
type="checkbox"
>
<span
class="app-switch-slider"
:class="{ 'is-checked': isChecked }"
></span>
</label>
</template>
<style lang="scss" scoped>
.app-switch {
//...
&.is-disabled {
//...
}
}
.app-switch-input {
//...
&:focus-visible {
// That nesting is pretty deep, that's why
// the `.app-switch-input` selector is moved to the root
~ .app-switch-slider {
//...
}
}
}
.app-switch-slider {
//...
&:after {
//...
}
&.is-checked {
//...
&:after {
//...
}
}
}
</style>
<template>
<section class="comment-thread">
<h3 class="comment-thread-title">Comments</h3>
<ul class="comment-thread-list">
<li
v-for="comment in comments"
:key="comment.id"
class="comment-thread-list-item"
>
<div class="comment-item">
<div class="comment-item-header">
<span class=comment-item-header-author">{{ comment.author }}</span>
<span class=comment-item-header-date">{{ comment.date }}</span>
</div>
<p class="comment-item-message">
{{ comment.message }}
</p>
<div class="comment-item-footer">
<button
class="comment-item-footer-reply-button button is-tertiary"
type="button"
>
Reply
</button>
<span class="comment-item-footer-likes">{{ comment.likes }} likes</span>
</div>
</div>
</li>
</ul>
</section>
</template>
<style lang="scss" scoped>
@use '@/styles/utils' as utils;
.comment-thread {
//...
.comment-thread-title {
//...
@include utils.text-header;
}
}
.comment-thread-list {
//...
.comment-thread-list-item {
//...
}
}
.comment-item {
//...
.comment-item-message {
//...
}
}
.comment-item-header {
//...
.comment-item-header-author {
//...
}
.comment-item-header-date {
//...
}
}
.comment-item-footer {
//...
.comment-item-footer-reply-button {
//...
}
.comment-item-footer-likes {
//...
}
}
</style>
<template>
<div class="app-modal">
<div class="app-modal-backdrop"></div>
<div class="app-modal-scroll-area">
<div
class="modal-content"
:class="{ 'is-small': isSmall, 'is-medium': isMedium, 'is-huge': isHuge }"
>
<button
class="modal-content-close-button button is-icon is-tertiary"
type="button"
>
<AppIcon :name="AppIconName.CLOSE" />
</button>
<form class="modal-content-main">
<div class="modal-header">
<h2 class="modal-header-title">
<slot name="title"></slot>
</h2>
<div class="modal-header-description">
<slot name="description"></slot>
</div>
</div>
<div class="modal-content-main-body">
<slot></slot>
</div>
<div
class="modal-content-footer"
:class="{ 'is-left': isFooterAlignedLeft }"
>
<slot name="footer">
<button
class="modal-content-footer-cancel-button button"
:class="{ 'is-error': isError, 'is-tertiary is-large': isTablet }"
type="button"
>
<slot name="cancel-button-text">
Cancel
</slot>
</button>
<button
class="modal-content-footer-confirm-button button"
:class="{ 'is-error': isError }"
type="button"
>
<slot name="confirm-button-text">
Confirm
</slot>
</button>
</slot>
</div>
</form>
<div class="modal-content-aside">
<slot name="aside"></slot>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/utils' as utils;
.app-modal {
//...
.app-modal-backdrop {
//...
}
.app-modal-scroll-area {
//...
}
}
.modal-header {
//...
.modal-header-icon {
//...
}
.modal-header-title {
//...
}
}
.modal-content {
//...
@include utils.tablet {
//...
}
&.is-small {
//...
}
&.is-medium {
//...
}
&.is-huge {
//...
}
.modal-content-close-button {
//...
}
.modal-content-main {
//...
@include utils.tablet {
//...
}
}
.modal-content-aside {
//...
}
.modal-content-footer {
//...
&.is-left {
//...
}
@include utils.tablet {
//...
}
}
}
</style>
Feature | Fluffy CSS | BEM | SMACSS | RSCSS | Atomic CSS |
---|---|---|---|---|---|
Readability | High | Medium | Medium | High | Low |
Naming Verbosity | Moderate | High | Moderate | Low | Low |
Modifier Syntax | is-* , has-* |
--* |
is-* |
_modifier |
Utility-first |
Learning curve | Low | Medium | Medium | Low | Medium |
Created by Mike Zotov
If you find this project helpful, feel free to support it:
- ERC-20/BNB/Base:
0x4e45bc70931bdcbf9afebb193fce04cf76703016
- Solana:
J3xH9qfGvz4UBmdyS4JvLka4zzSK8ui6yugkdqDwDEtq
- Bitcoin:
bc1qkmm4na0fmv4lgwgtftqwdsfzfpfpv7zqulft5f
MIT Β© Mike Zotov