Mixin (recipe)
// scss/config/mixins/_table.scss
@mixin table-base {
@include w-full;
@include bg-primary;
@include rounded-lg;
// ... styles
}
Complete reference of @include mixins from scss/config/_mixins.scss. Ashlar separates styling into two layers: a mixin (the recipe) and a component (the default application). Import with @use 'config/mixins' as *; and apply recipes directly to your own semantic selectors.
Every visual style has two layers: a mixin (recipe) and a component (default application).
// scss/config/mixins/_table.scss
@mixin table-base {
@include w-full;
@include bg-primary;
@include rounded-lg;
// ... styles
}
// scss/components/_table.scss
table {
@include table-base;
}
@media (max-width: 768px) {
table { @include table-responsive; }
}
| Situation | Mixin | Component selector |
|---|---|---|
Universal element (label, table, input) | yes | table { @include table-base; } |
| Page-level singleton | yes | .breadcrumbs { @include breadcrumbs; } |
Component class (.btn, .collapsible) | yes | .btn { @include btn; } |
Data-attr JS ([data-ln-tabs]) | not needed | [data-ln-tabs] nav { ... } |
| Need | Approach | Example |
|---|---|---|
| Use default | Do nothing — library CSS works | <table>...</table> |
| Different color | Override CSS variable on parent | #danger-zone { --color-primary: var(--color-error); } |
| Different structure | Re-apply mixin with modifications | #audit-log { @include table-base; @include table-striped; } |
| Full replace | Use only the mixin on custom selector | Exclude component, write from scratch |
Mixins NEVER contain concrete color values (#fff, rgb(), hsl(0 0% 100%)). Every color reads from a CSS custom property token: var(--color-*). This is what makes the entire system work — colors are inherited through the CSS cascade, not baked into mixin definitions.
// Hardcoded — breaks inheritance
.ln-modal header {
background-color: hsl(var(--color-primary));
border-bottom: none;
color: #fff;
}
// Mixin reads tokens, project just uses it
.ln-modal header {
@include panel-header;
}
// Want a different color? Override the token
.ln-modal {
--color-primary: 280 60% 50%;
// header automatically inherits
}
| Need | Approach | Example |
|---|---|---|
| Different color | Override CSS variable on parent element | .ln-modal { --color-primary: 280 60% 50%; } |
| Different structure (padding, layout, shape) | Override the mixin in project _overrides.scss |
@mixin panel-header { @include px(2rem); ... } |
| Both | Override mixin for structure + CSS variable for color | Mixin override + .parent { --color-primary: ...; } |
// scss/config/_mixins.scss (ln-ashlar)
@mixin btn {
@include flex;
@include items-center;
@include px(1rem);
@include py(0.5rem);
@include text-sm;
@include rounded-md;
@include cursor-pointer;
@include transition;
border: none;
outline: none;
white-space: nowrap;
}
// project/scss/_overrides.scss
@mixin btn {
@include inline-flex;
@include items-center;
@include px(1.5rem);
@include py(0.75rem);
@include text-base; // larger text
@include font-medium;
white-space: nowrap;
}
// Project selectors — UNCHANGED
#save-user { @include btn; }
#delete-user { @include btn; }
.form-actions button { @include btn; }
// project/scss/main.scss
// 1. Import ln-ashlar (loads default mixins)
@use 'ln-ashlar/scss/ln-ashlar';
// 2. Import project overrides AFTER ln-ashlar
// This file redefines mixins with the same name
@use 'overrides';
// 3. Import project pages (use the mixins — they get the overridden version)
@use 'pages/users';
@use 'pages/dashboard';
Accept any CSS value or var(--size-*) token.
| Mixin | Output |
|---|---|
@include p($val) | padding: $val |
@include px($val) | padding-left + padding-right |
@include py($val) | padding-top + padding-bottom |
@include pt($val), pb, pl, pr | Individual padding sides |
@include m($val) | margin: $val |
@include mx($val) | margin-left + margin-right |
@include my($val) | margin-top + margin-bottom |
@include mt($val), mb, ml, mr | Individual margin sides |
@include gap($val) | gap: $val |
article {
@include px(var(--size-lg));
@include py(var(--size-md));
@include mb(1rem);
}
| Mixin | Output |
|---|---|
@include flex | display: flex |
@include inline-flex | display: inline-flex |
@include block | display: block |
@include inline-block | display: inline-block |
@include hidden | display: none |
@include flex-col | display: flex; flex-direction: column |
@include flex-row | display: flex; flex-direction: row |
@include flex-wrap | flex-wrap: wrap |
@include flex-1 | flex: 1 |
@include flex-shrink-0 | flex-shrink: 0 |
@include items-center | align-items: center |
@include items-start | align-items: flex-start |
@include items-end | align-items: flex-end |
@include justify-center | justify-content: center |
@include justify-between | justify-content: space-between |
@include justify-end | justify-content: flex-end |
@include flex-center | display: flex; align-items: center; justify-content: center |
| Mixin | Output |
|---|---|
@include w-full | width: 100% |
@include h-full | height: 100% |
@include min-h-screen | min-height: 100vh |
@include size($val) | width: $val; height: $val |
| Mixin | Output |
|---|---|
@include text-xs | font-size: var(--text-xs); line-height: 1rem |
@include text-sm | font-size: var(--text-sm); line-height: 1.25rem |
@include text-base | font-size: var(--text-base); line-height: 1.5rem |
@include text-lg | font-size: var(--text-lg); line-height: 1.75rem |
@include text-xl | font-size: var(--text-xl); line-height: 1.75rem |
@include text-2xl | font-size: var(--text-2xl); line-height: 2rem |
@include font-normal | font-weight: var(--font-normal) |
@include font-medium | font-weight: var(--font-medium) |
@include font-semibold | font-weight: var(--font-semibold) |
@include font-bold | font-weight: var(--font-bold) |
@include text-left, text-center, text-right | Text alignment |
@include uppercase, lowercase, capitalize, normal-case | Text transform |
@include tracking-wide | letter-spacing: 0.025em |
@include tracking-wider | letter-spacing: 0.05em |
@include truncate | overflow: hidden; text-overflow: ellipsis; white-space: nowrap |
@include whitespace-nowrap | white-space: nowrap |
@include font-mono | font-family: var(--font-mono) |
@include font-sans | font-family: var(--font-sans) |
| Mixin | Output |
|---|---|
@include text-primary | color: var(--color-fg) |
@include text-white | color: hsl(var(--color-white)) |
@include text-error | color: hsl(var(--color-error)) |
@include text-success | color: hsl(var(--color-success)) |
@include text-warning | color: hsl(var(--color-warning)) |
@include bg-primary | background-color: var(--color-bg) |
| Mixin | Output |
|---|---|
@include border | border: 1px solid hsl(var(--color-border)) |
@include border-t, border-b, border-l, border-r | Individual border sides |
@include border-none | border: none |
@include rounded-sm | border-radius: var(--radius-sm) — 2px |
@include rounded-md | border-radius: var(--radius-md) — 6px |
@include rounded-lg | border-radius: var(--radius-lg) — 8px |
@include rounded-xl | border-radius: var(--radius-xl) — 12px |
@include rounded-full | border-radius: 9999px |
| Mixin | Output |
|---|---|
@include shadow-none | No shadow |
@include shadow-xs | Very subtle depth (0 1px 2px) |
@include shadow-sm | Subtle multi-layer shadow |
@include shadow-md | Medium shadow |
@include shadow-lg | Large shadow |
@include shadow-xl | Extra large shadow |
| Mixin | Output |
|---|---|
@include transition | transition: all var(--transition-base) |
@include transition-fast | transition: all var(--transition-fast) |
@include transition-slow | transition: all var(--transition-slow) (0.3s) |
@include transition-colors | Transitions only color, background-color, border-color |
| Mixin | Output |
|---|---|
@include relative, absolute, fixed, sticky | Position values |
@include sticky-top($top: 0) | position: sticky; top: $top; z-index: var(--z-sticky) |
@include inset-0 | top: 0; right: 0; bottom: 0; left: 0 |
@include overflow-hidden | overflow: hidden |
@include overflow-auto | overflow: auto |
@include overflow-x-auto | overflow-x: auto with touch scrolling |
| Mixin | Output |
|---|---|
@include cursor-pointer | cursor: pointer |
@include cursor-not-allowed | cursor: not-allowed |
@include select-none | user-select: none |
@include opacity-50 | opacity: 0.5 |
| Mixin | Output |
|---|---|
@include z-dropdown | z-index: var(--z-dropdown) |
@include z-sticky | z-index: var(--z-sticky) |
@include z-overlay | z-index: var(--z-overlay) |
@include z-modal | z-index: var(--z-modal) |
@include z-toast | z-index: var(--z-toast) |
@include z($val) | z-index: $val |
Parent declares the container context, child queries it with native @container. Never combine container-type with overflow: hidden on the same element.
| Mixin | Output |
|---|---|
@include container | container-type: inline-size |
@include container(name) | container-type: inline-size; container-name: name |
// Parent — declares the context
#sidebar { @include container(sidebar); }
// Child — queries the container
#sidebar nav {
@container sidebar (min-width: 580px) {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
| Width | Use |
|---|---|
| 480px | 1 → 2 columns (narrow containers) |
| 580px | 1 → 2 columns (standard) |
| 880px | 2 → 3 columns |
| 1120px | 3 → 4 columns |
Grid-based expand/collapse animation. Uses grid-template-rows: 0fr/1fr transition.
| Mixin | Role |
|---|---|
@include collapsible | Parent — display: grid with 0fr → 1fr transition. Toggled by .open class |
@include collapsible-content | Child — overflow: hidden to clip content |
.my-panel { @include collapsible; }
.my-panel > section { @include collapsible-content; }
| Mixin | Columns | Responsive |
|---|---|---|
@include grid | 1 → 2 → 3 | 768px → 2col, 1024px → 3col |
@include grid-2 | 1 → 2 | 768px → 2col |
@include grid-4 | 1 → 2 → 4 | 768px → 2col, 1024px → 4col |
@include form-grid | 6-column grid | <768px → 1col. Children use grid-column: span N |
@include stack($gap) | Flex column | Vertical stack with configurable gap (default: 1rem) |
// Responsive grid
#products { @include grid; }
// Form layout — 6 columns
#my-form {
@include form-grid;
> label { grid-column: span 3; } // half
> label:nth-child(3) { grid-column: span 6; } // full
> .form-actions { grid-column: span 6; }
}
| Mixin | Description |
|---|---|
@include card | White bg, light border, shadow-sm, rounded, hover shadow-md |
@include card-accent-top | 3px colored border-top (reads --color-primary) |
@include card-accent-bottom | 3px colored border-bottom |
@include card-accent-left | 3px colored border-left (side status) |
@include card-bg | Soft tinted background from --color-primary |
@include card-stacked | Pseudo-element "stacked cards" depth illusion |
@include panel-header | Flex header bar with border-bottom. Styles nested h3 |
@include section | Margin-bottom + header with border-bottom, styles h2 and .section-actions |
@include section-card | Complete card with header (panel-header), main (padding), footer (border-top) |
// Semantic usage
#user-profile { @include section-card; }
// Card variants — color via --color-primary override
#alerts { @include card; @include card-accent-top; --color-primary: var(--color-error); }
#status { @include card; @include card-accent-left; --color-primary: var(--color-success); }
#welcome { @include card; @include card-bg; }
#archive { @include card; @include card-stacked; }
Two-tier system: structure is global, color is semantic.
| Mixin | Description |
|---|---|
@include btn | Complete action button: structure (inline-flex, padding, font) + filled primary color. For non-submit action buttons via project SCSS. |
@include btn-sm | Size modifier: less padding, text-xs. Apply after @include btn. |
@include btn-lg | Size modifier: more padding, text-base. Apply after @include btn. |
@include btn-group | Row action button group: inline-flex, small gap, no list style. A .btn-group utility class is also shipped for prototyping (same as .btn) — production uses @include on a semantic selector. |
| Tier | How | Result |
|---|---|---|
| 1. Neutral | <button type="button"> — no class | Transparent bg, muted text, gray hover. Cancel/Close/Toggle. |
| 2. Primary | <button type="submit"> — no class | Primary fill color + white text. Color-only override on top of tier 1 structure. |
| 3. Action | @include btn on a semantic selector (or class="btn" for prototyping) | Filled primary color. Same as tier 2 but for non-submit action buttons. |
--color-primarySet --color-primary on the element or any parent. All states (hover, active, focus-ring) adapt automatically.
/* Project semantic selectors — correct approach */
#delete-user { @include btn; --color-primary: var(--color-error); }
#approve-user { @include btn; --color-primary: var(--color-success); }
/* Context override — all submit + action buttons inside change */
.danger-zone { --color-primary: var(--color-error); }
/* Custom brand color */
:root { --color-brand: 280 60% 50%; }
.brand-section { --color-primary: var(--color-brand); }
All form mixins live in scss/config/mixins/_form.scss. The component file scss/components/_form.scss applies them to default element selectors.
| Mixin | Description |
|---|---|
@include form-label | Block label: text-sm, font-medium, text-primary, margin-bottom |
@include form-grid | 6-column grid → 1-column on mobile. Children use grid-column: span N |
@include form-actions | Flex row with justify-end, gap, border-top, margin-top |
@include form-input | Base input styling: border, focus ring, placeholder, disabled state |
@include form-textarea | Min-height 6rem, vertical resize |
@include form-select | Custom appearance with SVG arrow indicator |
@include form-input-icon-group | Label wrapping icon(s) + input. Handles flex layout and focus ring |
@include form-check | Base checkbox/radio: appearance none, custom border, checked state |
@include form-checkbox | Checkbox: form-check + rounded-sm corners |
@include form-radio | Radio: form-check + circle + dot indicator |
@include form-validate-invalid | Invalid input: error border color + error focus ring |
@include form-validate-valid | Valid input: success border color + success focus ring |
@include form-validate-errors | Error message list: no bullets, caption size, error color, top margin |
@include pill-outline | Base pill label: structure + visible border + visible input indicator |
@include pill | Filled pill label: extends pill-outline, gray bg, colored on checked, input hidden |
@include pill-group | Joined pills (base modifier): border-radius on first/last child only |
@include pills | Joined-group filled pills: apply to <ul> containing <li><label><input> |
@include pills-outline | Joined-group outline pills: apply to <ul> containing <li><label><input> |
@include toggle-switch | Base switch element (applied to <input type="checkbox">) |
@include pill-switch | Switch label: layout with text + switch |
@include pills-switch | Vertical list of switches (applied to <ul>) |
@include check-list-outline | Vertical label list with pill-outline labels (visible input, bordered) |
@include check-list | Vertical label list with filled pill labels (extends check-list-outline) |
// Library default — already applied
input, textarea, select { @include form-input; }
input[type="checkbox"] { @include form-checkbox; }
label { @include form-label; }
// Project override — custom form
#audit-form {
@include form-grid;
label { grid-column: span 3; }
.form-actions { grid-column: span 6; }
}
// Pill outline variant — standalone on label
#role-selector label { @include pill-outline; }
All table mixins live in scss/config/mixins/_table.scss. The component file applies table-base and table-responsive to the table element.
| Mixin | Description |
|---|---|
@include table-base | Full table styling: bg, shadow, rounded, thead, th, td, row hover, last-row border |
@include table-responsive | Mobile stacked layout: td becomes block, uses data-label for column headers |
@include table-striped | Odd-row zebra striping |
@include table-section-header | Section header row: bold, colored background |
@include table-action | Inline action button/link in table cells |
// Library default — already applied
table { @include table-base; }
@media (max-width: 768px) { table { @include table-responsive; } }
// Project — striped table with section headers
#server-status {
@include table-base;
@include table-striped;
tr[data-section] td { @include table-section-header; }
td a { @include table-action; }
}
| Mixin | Description | Default selector |
|---|---|---|
@include breadcrumbs | Flex list with » separator, link hover, [aria-current="page"] for active item | .breadcrumbs |
@include loader | CSS-only spinner animation (box-shadow based) | none — project defines selector |
// Breadcrumbs — library applies to .breadcrumbs
// HTML: <nav class="breadcrumbs" aria-label="Breadcrumb">
// <ol>
// <li><a href="#">Home</a></li>
// <li><a href="#">Users</a></li>
// <li aria-current="page">Edit</li>
// </ol>
// </nav>
// Loader — project applies to its own selector
#loading { @include loader; }
| Mixin | Description | Default selector |
|---|---|---|
@include avatar | Profile button/link with square-ish image (rounded-md) + name/role text. Hover bg, focus ring. Works on <button> and <a>. | .avatar |
// Avatar — profile button with image + name/role
<button class="avatar">
<img src="avatar.jpg" alt="">
<span>Sojic<small>Administrator</small></span>
</button>
// As dropdown trigger
<div data-ln-dropdown>
<button class="avatar" data-ln-toggle-for="user-menu">
<img src="avatar.jpg" alt="">
<span>Sojic<small>Administrator</small></span>
</button>
<ul id="user-menu" data-ln-toggle>
<li><a href="#">Edit Profile</a></li>
<li><a href="#">Logout</a></li>
</ul>
</div>
| Mixin | Description |
|---|---|
@include nav | General navigation reset: clean list, styled links (hover/active), .nav-icon, .nav-label, .nav-section, .nav-divider. Layout and background come from the parent. |
// Sidebar — parent handles layout & bg
aside { @include flex-col; @include h-full; @include bg-primary; }
aside nav { @include nav; }
// Header nav — same mixin, different context
#header nav { @include nav; }
// In HTML:
// <nav data-ln-nav="active">
// <ul>
// <li><a href="/">
// <span class="nav-icon ln-icon-home"></span>
// <span class="nav-label">Dashboard</span>
// </a></li>
// </ul>
// </nav>