Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

FSCSS @define: mixins that compile to boring, good CSS

In FSCSS (Figured Shorthand CSS), reusable blocks and micro-helpers are built with @define. A definition is a compile-time blueprint: parameters go in, @use(...) substitutes them, and the compiler emits normal CSS. You c

In FSCSS (Figured Shorthand CSS), reusable blocks and micro-helpers are built with @define. A definition is a compile-time blueprint: parameters go in, @use(...) substitutes them, and the compiler emits normal CSS. You can keep that blueprint in a helpers file, import it into a page stylesheet, rename it, and call it like a local shorthand.

That pipeline is easy to mock: β€œAll that ceremony for a box-shadow and some padding?” Fair question. Below is the minimal protocol, then why it exists when the output stays simple on purpose.

Step 1 β€” Define a micro

In something like helpers.fscss:

@define elevate(space: 12px) {
  box-shadow: 0 @use(space) 20px rgba(0, 0, 0, 0.1);
  padding: @use(space);
}

Default space is 12px. Call sites can override it.

For blocks that emit a selector wrapper, use a backticked body and put the selector in a parameter:

@define center-box(elem){`
  @use(elem) {
    display: flex;
    justify-content: center;
    align-items: center;
  }
`}

@center-box(.hero) expands into a real rule for .hero, not a property list dumped into the wrong place.

Step 2 β€” Import (and optionally rename)

In main.fscss:

@import((elevate as shadow) from "./helpers.fscss")

Named import pulls only what you need. as is a local rename: in this file the helper is @shadow, not @elevate. The original name is not available here after the alias. That is deliberate β€” it avoids collisions when two modules both ship something called card or root.

Same idea works for registry modules and remote paths; the contract is the same: register defines, then call them.

Step 3 β€” Call it

.card {
  @shadow(16px);
}

Compile once. Output is ordinary CSS:

.card {
  box-shadow: 0 16px 20px rgba(0, 0, 0, 0.1);
  padding: 16px;
}

No runtime mixin engine. No framework component. The browser only ever sees the expanded sheet.

β€œWhy all that protocol for such simple CSS?”

For one card on one page, the protocol is overkill. Write the two properties by hand and move on. FSCSS does not require @define for every rule.

The layers start to earn their keep when the project grows.

1. Change it once

Forty surfaces share the same elevation: cards, modals, menus, chips. Design tightens the blur or the offset.

  • Plain copy-paste CSS: you hunt every occurrence, miss two, ship inconsistency.
  • One @define: edit the blueprint, recompile, every call site updates.

The output is still β€œsimple CSS.” The win is where the source of truth lives.

2. One argument, several properties

In elevate, a single space drives both shadow offset and padding:

@define elevate(space: 12px) {
  box-shadow: 0 @use(space) 20px rgba(0, 0, 0, 0.1);
  padding: @use(space);
}

Call @shadow(16px) and both sides stay in proportion. Native custom properties are excellent for values, but they do not replace a compile-time template that stitches the same token into different property positions without repeating the call site. Mixins are for structure + substitution; variables are for runtime-tunable values. Use both when each fits.

3. Design-system guardrails

Teams drift toward magic numbers: box-shadow: 0 3px 14px … next to 0 8px 24px … with no shared scale. Approved micros β€” @elevate(sm)-style patterns, or a small set of named helpers β€” push people toward the same vocabulary. Consistency becomes the default path, not a style-guide PDF nobody opens.

Modules in the wider ecosystem (charts, forms, AI chrome, overlap stacks) are the same idea at library scale: publish @defines, import what you need, compile to CSS the browser already understands.

Mental model

Piece Role
@define Publish a parameterized blueprint
@use(name) Insert the argument at compile time
@import((…)) Bring defines into this file
as Local rename; original name unused here
@name(…) Run substitution β†’ CSS

@import does not β€œrun” the module. It exposes registered defines. Communication finishes when you invoke the mixin.

When to skip it

  • One-off layout
  • Prototype you will throw away
  • A value that truly appears once

Write standard CSS (or FSCSS shorthands on a single rule) and do not invent a helper β€œjust because.”

When the same multi-property pattern shows up the third time, extract an @define. When another file needs it, import. When two libraries collide on a name, alias with as.

Closing

The protocol looks heavy only if you judge it by a single compiled rule. Judge it by how many times that rule would have been duplicated, and by whether one parameter keeps related properties in sync.

FSCSS @define is not trying to make CSS clever at runtime. It is trying to make your source maintainable, then emit the boring CSS you would have written by hand β€” once, correctly, everywhere it is needed.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.