Imagine maintaining a documentation website. Its article cards have a thin border, rounded corners, and generous padding. A release notice beside them needs almost the same frame. You copy a few lines of CSS, change the border color, and both components look right.
A few weeks later, the design calls for different spacing across the site. You update the cards but miss one of the notices. It is tempting to think that extracting a mixin earlier would have prevented the mistake.
Another outcome is just as plausible: the notice needs tighter spacing, while the card still needs room for comfortable reading. If both are tied to one abstraction, a shared change could now affect a component that should have stayed the same.
Repeated code is a clue. The next question is whether those styles express the same rule and should change together. Native CSS @mixin is interesting because it proposes a way to name a group of style rules and supply differences where that group is used.
Find the rules that should change together
For this hypothetical documentation site, let us make the requirement more precise. Article cards and release notices both need a quiet frame with consistent corners. Their borders and headings share an accent color, while notices may use tighter padding.
The containers carry different content but share part of their visual treatment. Removing repeated lines is only one concern. We also need to decide which choices deserve a common source and which belong to individual components.
The corner radius, for example, may express a site-wide convention. Padding may depend on content density. Whether a card shows an excerpt or a notice includes an action link belongs to each component. Sharing a frame does not require taking responsibility for those decisions too.
This distinction determines the size of the abstraction. If we cannot yet explain what is shared, a few repeated declarations may be easier to adjust. Actual uses give us evidence for extracting the parts that remain stable.
How far can custom properties and shared classes go?
Start without a mixin. The examples below use this common HTML structure:
<article class="site-card">
<h2 class="site-panel-title">Installation guide</h2>
<p>Start with your project's first page.</p>
</article>
<aside class="site-notice">
<h2 class="site-panel-title">Release notice</h2>
<p>Read the changelog before upgrading.</p>
</aside>Selector grouping and custom properties are enough to centralize the frame. This example uses existing CSS:
:root {
--panel-radius: 0.5rem;
}
.site-card,
.site-notice {
border: 1px solid var(--panel-accent, #777777);
border-radius: var(--panel-radius);
padding: var(--panel-space, 1.5rem);
}
.site-card > .site-panel-title,
.site-notice > .site-panel-title {
margin-block: 0 0.75rem;
color: var(--panel-accent, #777777);
}
.site-notice {
--panel-accent: #45687d;
--panel-space: 1rem;
}Two forms of reuse work together here. Grouped selectors share declarations, while custom properties supply values that can vary. var() substitutes a variable into a property value. It does not insert an entire group of border, padding, and nested rules into another rule. The custom properties specification defines that substitution mechanism.
If you control the HTML, you could move the shared rules into .site-panel and use class="site-panel site-card" or class="site-panel site-notice". One class expresses the frame; the other expresses the component's role. Custom properties still control the accent and padding.
For a small site with just these two containers, I would start there. The declarations are centralized, the differences are visible, and there is no additional call to follow. A third container can use the same class if it really shares that frame.
A mixin offers another organization when shared rules need to be composed inside several component styles, when each use needs explicit configuration, or when adding a common class to HTML from different sources is inconvenient. The application stays in CSS, with a name indicating which group of rules a component has chosen.
| What you want to reuse | Consider first | The relationship to maintain |
|---|---|---|
| Values such as colors, spacing, and radii | Custom properties | Where values come from, how they inherit, and where they are overridden |
| The same set of declarations | A shared class or grouped selectors | Which elements should receive the same changes |
| A parameterized style block composed inside different rules | The native mixin proposal | Call sites, parameters, and the block's responsibility |
These approaches can work together. A mixin can reference design tokens, and a shared class can apply a mixin. The useful choice is the one that makes responsibility for changes clearer.
Define a rule with @mixin and apply it with @apply
Here is a minimal mixin version of the frame. This and the subsequent mixin examples use draft syntax:
/* Draft syntax: definition and application */
@mixin --panel-frame() {
border: 1px solid #777777;
border-radius: 0.5rem;
padding: 1.5rem;
}
.site-card {
@apply --panel-frame();
}
.site-notice {
@apply --panel-frame();
}@mixin defines a reusable block with a name beginning with --; @apply identifies where to use it. Under the draft's definition and application rules, a mixin cannot be defined inside an ordinary style rule. An @apply belongs inside a style rule or a nested group rule within it. The example puts the definition at the top level and the calls inside the selectors.
A useful starting model is that the declarations and rules are introduced at the call site. That is a reading aid, though. Parameters, variables, and scope mean this should not be treated as arbitrary text substitution.
The current draft allows parentheses to be omitted when there are no parameters or arguments, as in @apply --panel-frame;. This article keeps them to make calls recognizable and consistent with the parameterized version that follows. The name passed to @apply identifies a mixin definition, not a class.
Next, make the accent and padding configurable. The following complete CSS replaces the previous definition and still uses the HTML above:
/* Draft syntax: parameters, defaults, and nested rules */
@mixin --panel-frame(
--accent <color>: #777777,
--space <length>: 1.5rem
) {
border: 1px solid var(--accent);
border-radius: 0.5rem;
padding: var(--space);
& > .site-panel-title {
margin-block: 0 0.75rem;
color: var(--accent);
}
}
.site-card {
@apply --panel-frame();
}
.site-notice {
@apply --panel-frame(#45687d, 1rem);
}--accent accepts a color and --space a length. The values after the colons are defaults for omitted arguments. The card uses those defaults; the notice supplies a blue-gray accent and tighter padding. Types and defaults follow the draft's parameter syntax: 1.5rem, for example, is a <length>, while a percentage is not.
There are deliberately only two parameters. They correspond to the differences we already identified. The corner radius remains a shared rule, and the heading follows the frame's accent. Reading a call shows which variation a component uses.
Although parameters are read through var(), they do not simply become ordinary, public custom properties on the element. In the current parameter model, a parameter shadows an outside value with the same name, and its private scope limits which elements can access it. This example styles the container and its children. If a nested rule selects a sibling instead, it cannot assume that the parameter will remain available there.
The nested & > .site-panel-title keeps the relationship between the frame and heading colors in one rule. It also establishes a structural contract: the heading must be a direct child of the container. If some components do not have that structure, separating the heading treatment from the frame may be clearer than expanding the selector to accommodate every case.
Share a condition with @contents
Now add a requirement: cards and notices both change on narrow screens, but their changes differ. Cards need a more compact arrangement; notices need smaller padding and a stronger border on the inline-start side.
If they genuinely share a page-wide compact-layout condition, we can name that condition and leave a place for caller-supplied styles with @contents. This draft example follows the CSS from the previous section:
/* Draft syntax: shared condition, caller-supplied content */
@mixin --compact-layout() {
@media (width <= 40rem) {
@contents;
}
}
.site-card {
@apply --compact-layout() {
display: grid;
gap: 0.75rem;
padding: 1rem;
}
}
.site-notice {
@apply --compact-layout() {
padding: 0.75rem;
border-inline-start-width: 3px;
}
}The block after @apply is the content supplied by the caller. @contents marks where it goes. The draft's @contents rule allows this kind of block to be passed to a mixin. What is shared is the decision about when the compact layout applies. Each component keeps its own response to that decision nearby.
The 40rem threshold belongs to this hypothetical layout. It is not a universal device category. If cards and notices should respond to the space available in different containers, sharing a single viewport threshold may no longer make sense. Reconsider the condition instead of retaining it simply because a mixin already exists.
An @contents rule can also provide fallback content, used only when the caller supplies no block at all. Passing an empty block counts as supplying content; that content is simply empty. Our --compact-layout() has no fallback, so invoking it without a content block adds no declarations to the component.
For just these two components, a single @media containing both style rules would also be easy to understand. A condition mixin becomes useful when several places share a meaningful layout decision and you want their styles to remain close to each component. The extra name should make the intent clearer.
The cost of an abstraction appears when you change it
Suppose the site gains promotional cards, error notices, and sidebar summaries. It is tempting to add border width, background, icon position, and heading size to --panel-frame(), followed by a few switches for special cases.
As the parameter list grows, examine how those options relate. If most callers override half the settings, or a group of parameters serves only one component, the shared frame may have become a collection of unrelated requirements. Smaller rules, or even declarations kept inside individual components, may be easier to maintain.
I would check that boundary with three questions:
- Should these changes happen together? Does a radius adjustment belong to every container or only to one kind of content?
- Does the call explain its intent? An accent and a spacing value are easy to read. A long list of positional arguments sends readers back to the definition.
- Where is an exception clearest? A stronger side border used only by notices can stay in the notice rule without becoming a site-wide option.
A mixin asks maintainers to connect the call site, its definition, and the supplied values. When it calls other mixins, that path gets longer. The convenience of centralized changes needs to justify this reading cost.
Applied declarations also have to coexist with the rest of the stylesheet. A mixin does not remove cascade questions. In this simple example, placing a component's own border-radius declaration after @apply can express a local difference. If another source sets that property too, you still need to examine origin, layer, specificity, and order. An abstraction should not make the effective value harder to explain.
Both classes and mixins establish relationships between elements that will receive shared changes. Similar appearance today is not enough evidence that they should remain synchronized. Finding a meaningful name for the rule before choosing a mechanism is usually more reliable than minimizing line count first.
What is worth learning now?
If you know Sass, the broad idea will be familiar. However, Sass mixins are applied with @include, use $ parameters, and are processed by Sass into CSS. The native proposal uses @apply, -- names, and CSS's value system. Similar purposes do not make syntax, scope, or migration interchangeable.
The same CSS draft also defines @function, which works with values—for example, calculating a length for use in padding. A mixin works with a style block that can contain several declarations and nested rules. The level at which you need reuse helps determine which capability fits.
The draft itself is still being revised. This article follows the definitions in sections 5.1 through 5.4. Section 5.5 still carries a note to update its explanation for the new private-property model and retains older @result syntax. Accordingly, this article does not treat those internal substitution steps as settled behavior or its syntax explanations as a compatibility guarantee.
Returning to the documentation site, I would still begin with a shared class or grouped selectors, using custom properties for explicit differences in values. When rules need composition across components and a clear parameter interface, I would evaluate whether a mixin makes that relationship easier to express.
Native @mixin points toward a useful extension of CSS reuse: moving from individual values to named, configurable groups of rules. Making good use of that capability still starts with the original question: when the next change arrives, which styles should change together?
