Composition mixins
Composition mixins are high-level shortcuts for patterns you write constantly. Rather than reaching for multiple mixins, a single mixin can handles the whole thing when needed. All parameters are optional, so you can use only what you need. Absolutely not an idea stolen borrowed from SwiftUI
Stacks
vstack and stack
vstack($gap, $reverse: false) and stack($gap, $reverse: false)
Stack children vertically with a consistent gap from the spacing scale. Pass $reverse: true to use column-reverse.
.card-list {
@include crayon.vstack(4);
}
.sidebar {
@include crayon.stack(6); /* same thing, different name */
}
.newest-first {
@include crayon.vstack(4, $reverse: true);
}.card-list
+crayon.vstack(4)
.sidebar
+crayon.stack(6) /* same thing, different name */
.newest-first
+crayon.vstack(4, $reverse: true)Output: display: flex; flex-direction: column; gap: <spacing-scale-value>;, or flex-direction: column-reverse when reversed.
hstack
hstack($gap, $reverse: false)
Lay children out in a horizontal row with a gap. No wrapping, items stay on one line. Use cluster if you want items to wrap when space is tight. Pass $reverse: true to use row-reverse.
.toolbar {
@include crayon.hstack(3);
}
.toolbar-reversed {
@include crayon.hstack(3, $reverse: true);
}.toolbar
+crayon.hstack(3)
.toolbar-reversed
+crayon.hstack(3, $reverse: true)Output: display: flex; flex-direction: row; gap: <spacing-scale-value>;, or flex-direction: row-reverse when reversed.
cluster
cluster($gap)
Like hstack, but items wrap onto new lines when there isn't enough space. Great for tag lists, badge groups or anything that should reflow naturally.
.tags {
@include crayon.cluster(2);
}.tags
+crayon.cluster(2)Output: display: flex; flex-direction: row; flex-wrap: wrap; gap: <spacing-scale-value>;
Nesting composition mixins
The layout primitives (vstack, hstack, cluster) are composable. So you can do things like mae a vstack of sections, each containing an hstack of items:
.layout {
@include crayon.vstack(8);
.row {
@include crayon.hstack(4);
}
}.layout
+crayon.vstack(8)
.row
+crayon.hstack(4)Wrap a layout primitive in box to add visuals of the container:
.card {
@include crayon.box($p: 6, $rounded: "xl", $bg: "white", $border: "slate-200");
@include crayon.vstack(4);
}.card
+crayon.box($p: 6, $rounded: "xl", $bg: "white", $border: "slate-200")
+crayon.vstack(4)Or don't. S'all good.
palette
palette($family, $bg, $text, $border)
Apply background, text, and border colours from a single colour family in one call. All three colour parameters are optional, any you don't need, just skip them.
The colour family is the prefix (e.g. "red"), and the shade is the number (e.g. 100, 900). These map directly to the colour palette: "red" + 100 → red-100.
/* Alert variants */
.alert-success { @include crayon.palette("green", $bg: 100, $text: 900, $border: 300); }
.alert-warning { @include crayon.palette("amber", $bg: 100, $text: 900, $border: 300); }
.alert-error { @include crayon.palette("red", $bg: 100, $text: 900, $border: 300); }
/* Badge — bg + text only, no border */
.badge-new { @include crayon.palette("emerald", $bg: 50, $text: 700); }
/* Dark mode */
.card {
@include crayon.palette("slate", $bg: 50, $text: 900, $border: 200);
@include crayon.dark {
@include crayon.palette("slate", $bg: 900, $text: 50, $border: 700);
}
}/* Alert variants */
.alert-success
+crayon.palette("green", $bg: 100, $text: 900, $border: 300)
.alert-warning
+crayon.palette("amber", $bg: 100, $text: 900, $border: 300)
.alert-error
+crayon.palette("red", $bg: 100, $text: 900, $border: 300)
/* Badge — bg + text only, no border */
.badge-new
+crayon.palette("emerald", $bg: 50, $text: 700)
/* Dark mode */
.card
+crayon.palette("slate", $bg: 50, $text: 900, $border: 200)
+crayon.dark
+crayon.palette("slate", $bg: 900, $text: 50, $border: 700)box
box($p, $px, $py, $rounded, $bg, $border)
Padding, border radius, background, and border in a single call. $px and $py can be used alongside or instead of $p for asymmetric padding (e.g. badges and buttons that need wider horizontal padding than vertical).
/* Card with full treatment */
.panel {
@include crayon.box($p: 6, $rounded: "xl", $bg: "white", $border: "slate-200");
@include crayon.dark {
@include crayon.box($bg: "slate-800", $border: "slate-700");
}
}
/* Asymmetric padding */
.badge {
@include crayon.box($px: 3, $py: 1, $rounded: "full", $bg: "blue-100");
}
/* Just a shape, no colour */
.card {
@include crayon.box($p: 4, $rounded: "lg");
}/* Card with full treatment */
.panel
+crayon.box($p: 6, $rounded: "xl", $bg: "white", $border: "slate-200")
+crayon.dark
+crayon.box($bg: "slate-800", $border: "slate-700")
/* Asymmetric padding */
.badge
+crayon.box($px: 3, $py: 1, $rounded: "full", $bg: "blue-100")
/* Just a shape, no colour */
.card
+crayon.box($p: 4, $rounded: "lg")$border sets border: 1px solid <colour> it doesn't control border width, just whether a border is present and what colour it is. For custom widths, combine with @include crayon.border(2).
center
center($axis?, $children: false)
Centers the element itself using auto margins. With no argument, it’ll center on both axes. Pass x or y to constrain it to one axis.
Set $children: true to use flexbox to center the element’s children instead. The same axis argument applies: x uses justify-content, y uses align-items, and no axis uses both.
/* Both axes */
.hero {
@include crayon.center;
}
/* Horizontal only */
.nav {
@include crayon.center(x);
}
/* Vertical only */
.sidebar {
@include crayon.center(y);
}
/* Center children on both axes */
.hero-content {
@include crayon.center($children: true);
}
/* Center children vertically */
.toolbar {
@include crayon.center(y, $children: true);
}/* Both axes */
.hero
+crayon.center
/* Horizontal only */
.nav
+crayon.center(x)
/* Vertical only */
.sidebar
+crayon.center(y)
/* Center children on both axes */
.hero-content
+crayon.center($children: true)
/* Center children vertically */
.toolbar
+crayon.center(y, $children: true)By default, center applies margin: auto, margin-inline: auto, or margin-block: auto, depending on the axis. With $children: true, it sets display: flex and applies the corresponding flex alignment properties.
At last, a solution to the hardest problem in software engineering. History has been made today, people.
💧Fluid mixin
The fluid() mixin generates a clamp() value that smoothly interpolates between two scale values across a viewport range. TL;DR - this
Basically, instead of text or spacing snapping between sizes at hard breakpoints, values scale continuously as the viewport changes size.
.hero {
@include crayon.fluid-text("base", "4xl");
/* → font-size: clamp(1rem, -0.25rem + 1.25vw, 2.25rem); */
}
/* Fluid spacing — numeric keys from the size scale */
.section {
@include crayon.fluid-p(4, 12);
/* → padding: clamp(1rem, -0.5rem + 1.5vw, 3rem); */
}
.sidebar {
@include crayon.fluid("width", 48, 80, "lg", "xl");
}.hero
+crayon.fluid-text("base", "4xl")
/* → font-size: clamp(1rem, -0.25rem + 1.25vw, 2.25rem); */
/* Fluid spacing — numeric keys from the size scale */
.section
+crayon.fluid-p(4, 12)
/* → padding: clamp(1rem, -0.5rem + 1.5vw, 3rem); */
.sidebar
+crayon.fluid("width", 48, 80, "lg", "xl")Parameters:
$propertyany CSS property that accepts a size (font-size,padding,gap,width, etc.)$min-keyminimum value. Numeric key (e.g.4) from the size scale. String (e.g."base") from the font-size scale.$max-keymaximum value, same rules.$frombreakpoint at which scaling starts. Named key from$breakpoints. Defaults to"sm"(640px).$tobreakpoint at which scaling stops. Named key from$breakpoints. Defaults to"xl"(1280px).
Shorthand wrappers:
| Mixin | Equivalent |
|---|---|
fluid-text($min, $max) | fluid("font-size", $min, $max) |
fluid-p($min, $max) | fluid("padding", $min, $max) |
fluid-px($min, $max) | fluid("padding", $min, $max) |
fluid-py($min, $max) | fluid("padding", $min, $max) |
fluid-gap($min, $max) | fluid("gap", $min, $max) |
All shorthands accept optional $from and $to as third and fourth arguments.
Why it's different from breakpoint mixins:
screen() gives you hard jumps. The value snaps from one size to another. fluid() gives you smooth transitions the value grows linearly between the two endpoints. Use fluid-text for headings and display copy. Use fluid-p for section padding that should feel generous on desktop but not wasteful on mobile.