Custom Functions with @function
Creating Custom Functions in SASS
While SASS provides a rich library of built-in functions, the real power comes from the ability to create your own custom functions using the @function directive. Custom functions allow you to encapsulate complex logic, perform calculations, and return reusable values that can be used throughout your stylesheets.
@function Syntax and @return
A SASS function is defined using the @function directive, followed by a name, parameters in parentheses, and a body containing the logic. Every function must use the @return directive to return a value.
Basic Function Syntax
// Simple function that doubles a number
@function double($number) {
@return $number * 2;
}
// Usage
.element {
width: double(50px);
// Result: width: 100px;
}
.another {
font-size: double(8px);
// Result: font-size: 16px;
}
// Function with multiple parameters
@function calculate-percentage($value, $total) {
@return ($value / $total) * 100%;
}
.progress-bar {
width: calculate-percentage(75, 100);
// Result: width: 75%;
}
.completion {
width: calculate-percentage(3, 5);
// Result: width: 60%;
}
Difference Between Functions and Mixins
Understanding when to use functions versus mixins is crucial for writing clean SASS code. The key difference is that functions return values while mixins output CSS blocks.
Functions Return Values
// Function returns a calculated value
@function calculate-rem($px-value) {
@return ($px-value / 16px) * 1rem;
}
// Use the returned value in properties
.heading {
font-size: calculate-rem(24px);
// Result: font-size: 1.5rem;
margin-bottom: calculate-rem(16px);
// Result: margin-bottom: 1rem;
}
// You can use function results in calculations
.container {
padding: calculate-rem(20px) + 0.5rem;
// Result: padding: 1.75rem;
}
Mixins Output CSS
// Mixin outputs CSS rules
@mixin heading-style {
font-size: 24px;
font-weight: 700;
margin-bottom: 16px;
color: #333;
}
// Including a mixin outputs all its CSS
.heading {
@include heading-style;
// Result:
// .heading {
// font-size: 24px;
// font-weight: 700;
// margin-bottom: 16px;
// color: #333;
// }
}
// You CANNOT use a mixin as a value
.bad-example {
font-size: @include heading-style; // ERROR!
}
When to Use Each
- Use Functions when:
- You need to calculate and return a single value
- You're performing unit conversions (px to rem, deg to rad)
- You're calculating colors, sizes, or other CSS values
- You need to use the result in multiple properties or calculations
- Use Mixins when:
- You need to output multiple CSS properties
- You're creating reusable style patterns
- You need to generate complete CSS blocks with nested selectors
- You're working with media queries or keyframes
Functions with Arguments and Default Values
Functions can accept multiple arguments and provide default values for optional parameters, making them flexible and reusable.
Default Parameter Values
// Function with default values
@function create-spacing($multiplier: 1, $base: 8px) {
@return $multiplier * $base;
}
// Using default values
.compact {
margin: create-spacing();
// Result: margin: 8px; (1 * 8px)
}
// Overriding first parameter
.spacious {
margin: create-spacing(3);
// Result: margin: 24px; (3 * 8px)
}
// Overriding both parameters
.custom {
margin: create-spacing(2, 10px);
// Result: margin: 20px; (2 * 10px)
}
// Named parameters for clarity
.explicit {
margin: create-spacing($multiplier: 4, $base: 5px);
// Result: margin: 20px;
}
// Skip first parameter, provide second
.different-base {
margin: create-spacing($base: 12px);
// Result: margin: 12px; (1 * 12px - uses default multiplier)
}
Multiple Parameters with Defaults
// Comprehensive spacing function
@function spacing($size: md, $direction: all) {
$sizes: (
xs: 4px,
sm: 8px,
md: 16px,
lg: 24px,
xl: 32px
);
$base-value: map-get($sizes, $size);
@if $direction == all {
@return $base-value;
} @else if $direction == vertical {
@return $base-value 0;
} @else if $direction == horizontal {
@return 0 $base-value;
} @else {
@return $base-value;
}
}
// Various uses
.box-1 {
padding: spacing();
// Result: padding: 16px;
}
.box-2 {
padding: spacing(lg);
// Result: padding: 24px;
}
.box-3 {
padding: spacing(md, vertical);
// Result: padding: 16px 0;
}
.box-4 {
padding: spacing($size: xl, $direction: horizontal);
// Result: padding: 0 32px;
}
Building Utility Functions: Unit Conversions
One of the most practical uses of custom functions is creating unit conversion utilities. These functions help maintain consistency and make your stylesheets more maintainable.
px-to-rem() Function
Pixel to REM Converter
// Global base font size
$base-font-size: 16px;
@function px-to-rem($px-value) {
// Remove unit if present
@if unitless($px-value) {
$px-value: $px-value * 1px;
}
// Calculate rem value
@return ($px-value / $base-font-size) * 1rem;
}
// Shorthand alias
@function rem($px-value) {
@return px-to-rem($px-value);
}
// Usage throughout stylesheet
body {
font-size: rem(16px);
// Result: font-size: 1rem;
}
h1 {
font-size: rem(32);
// Result: font-size: 2rem;
margin-bottom: rem(24);
// Result: margin-bottom: 1.5rem;
}
.card {
padding: rem(20) rem(30);
// Result: padding: 1.25rem 1.875rem;
border-radius: rem(8);
// Result: border-radius: 0.5rem;
}
// Advanced: Handle multiple values
@function rem-multiple($values...) {
$result: ();
@each $value in $values {
$result: append($result, rem($value));
}
@return $result;
}
.complex {
padding: rem-multiple(10px, 20px, 15px, 20px);
// Result: padding: 0.625rem 1.25rem 0.9375rem 1.25rem;
}
em() Function
Pixel to EM Converter (Context-Aware)
// Convert px to em based on parent context
@function em($px-value, $context: 16px) {
@if unitless($px-value) {
$px-value: $px-value * 1px;
}
@if unitless($context) {
$context: $context * 1px;
}
@return ($px-value / $context) * 1em;
}
// Usage with different contexts
.parent {
font-size: 20px;
.child {
font-size: em(16px, 20px);
// Result: font-size: 0.8em; (relative to 20px parent)
padding: em(10px, 20px);
// Result: padding: 0.5em;
}
}
.standard {
font-size: em(18px);
// Result: font-size: 1.125em; (relative to default 16px)
}
Modular Scale Function
Creating Typography Scale
// Modular scale for typography
$base-size: 16px;
$ratio: 1.25; // Major third scale
@function modular-scale($step) {
@return $base-size * pow($ratio, $step);
}
// Custom pow function for older SASS versions
@function pow($base, $exponent) {
$result: 1;
@if $exponent > 0 {
@for $i from 1 through $exponent {
$result: $result * $base;
}
} @else if $exponent < 0 {
@for $i from $exponent through -1 {
$result: $result / $base;
}
}
@return $result;
}
// Generate typography scale
h1 {
font-size: modular-scale(4);
// Result: 39.0625px (16 * 1.25^4)
}
h2 {
font-size: modular-scale(3);
// Result: 31.25px (16 * 1.25^3)
}
h3 {
font-size: modular-scale(2);
// Result: 25px (16 * 1.25^2)
}
h4 {
font-size: modular-scale(1);
// Result: 20px (16 * 1.25^1)
}
body {
font-size: modular-scale(0);
// Result: 16px (16 * 1.25^0)
}
small {
font-size: modular-scale(-1);
// Result: 12.8px (16 * 1.25^-1)
}
Building Color Helper Functions
Custom functions are excellent for creating consistent color systems and theme utilities.
Color Contrast Function
// Choose contrasting text color based on background
@function contrasting-color($background-color, $light: #fff, $dark: #000) {
// Calculate relative luminance
@if lightness($background-color) > 50% {
@return $dark;
} @else {
@return $light;
}
}
// Usage
.button-primary {
$bg: #007bff;
background-color: $bg;
color: contrasting-color($bg);
// Result: color: #fff; (dark background needs light text)
}
.button-warning {
$bg: #ffc107;
background-color: $bg;
color: contrasting-color($bg);
// Result: color: #000; (light background needs dark text)
}
// Advanced version with custom threshold
@function smart-contrast($bg, $threshold: 60%, $light: #fff, $dark: #000) {
@if lightness($bg) > $threshold {
@return $dark;
} @else {
@return $light;
}
}
.subtle-button {
$bg: #e0e0e0;
background-color: $bg;
color: smart-contrast($bg, $threshold: 70%);
}
Color Tint and Shade Functions
// Create tint (mix with white)
@function tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
// Create shade (mix with black)
@function shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
// Create tone (mix with gray)
@function tone($color, $percentage) {
@return mix(gray, $color, $percentage);
}
$brand-blue: #007bff;
.light-variant {
background: tint($brand-blue, 20%);
// 20% white mixed with brand blue
}
.lighter-variant {
background: tint($brand-blue, 40%);
// 40% white mixed with brand blue
}
.dark-variant {
background: shade($brand-blue, 20%);
// 20% black mixed with brand blue
}
.muted-variant {
background: tone($brand-blue, 30%);
// 30% gray mixed with brand blue
}
// Generate color palette
$primary: #3498db;
.palette {
&-tint-1 { background: tint($primary, 10%); }
&-tint-2 { background: tint($primary, 30%); }
&-tint-3 { background: tint($primary, 50%); }
&-tint-4 { background: tint($primary, 70%); }
&-tint-5 { background: tint($primary, 90%); }
&-base { background: $primary; }
&-shade-1 { background: shade($primary, 10%); }
&-shade-2 { background: shade($primary, 30%); }
&-shade-3 { background: shade($primary, 50%); }
&-shade-4 { background: shade($primary, 70%); }
&-shade-5 { background: shade($primary, 90%); }
}
Building Responsive Calculation Functions
Fluid Typography Function
// Calculate fluid font size using clamp
@function fluid-size($min-size, $max-size, $min-vw: 320px, $max-vw: 1200px) {
$slope: ($max-size - $min-size) / ($max-vw - $min-vw);
$y-intercept: $min-size - ($slope * $min-vw);
@return clamp(
$min-size,
#{$y-intercept} + #{$slope * 100}vw,
$max-size
);
}
// Usage
h1 {
font-size: fluid-size(24px, 48px);
// Scales from 24px to 48px between 320px and 1200px viewport
}
p {
font-size: fluid-size(14px, 18px);
// Scales from 14px to 18px
}
// Simplified clamp-based fluid sizing
@function fluid($min, $preferred, $max) {
@return clamp($min, $preferred, $max);
}
.responsive-text {
font-size: fluid(16px, 4vw, 32px);
}
Aspect Ratio Padding Function
// Calculate padding for aspect ratio boxes
@function aspect-ratio-padding($width, $height) {
@return ($height / $width) * 100%;
}
// Common aspect ratios
@function ar-16-9() {
@return aspect-ratio-padding(16, 9);
}
@function ar-4-3() {
@return aspect-ratio-padding(4, 3);
}
@function ar-square() {
@return aspect-ratio-padding(1, 1);
}
// Usage
.video-wrapper {
position: relative;
padding-bottom: ar-16-9();
// Result: padding-bottom: 56.25%;
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
.thumbnail {
position: relative;
padding-bottom: ar-4-3();
// Result: padding-bottom: 75%;
}
.avatar {
position: relative;
padding-bottom: ar-square();
// Result: padding-bottom: 100%;
}
Pure Functions and Side Effects
Functions in SASS should be pure, meaning they should only return values based on their inputs without modifying global state or causing side effects.
Good: Pure Function
// ✅ GOOD: Pure function
@function double($value) {
@return $value * 2;
}
// Always returns same output for same input
.element {
width: double(50px);
// Always: 100px
}
Bad: Function with Side Effects
// ❌ BAD: Function that modifies global variable
$global-counter: 0;
@function increment-and-return($value) {
$global-counter: $global-counter + 1; // Side effect!
@return $value + $global-counter;
}
// This is unpredictable - result depends on how many times called
.element-1 {
width: increment-and-return(10px);
// Result: 11px (counter is now 1)
}
.element-2 {
width: increment-and-return(10px);
// Result: 12px (counter is now 2) - UNEXPECTED!
}
// ✅ BETTER: Pass all needed values as parameters
@function calculate-with-offset($value, $offset) {
@return $value + $offset;
}
.element-1 {
width: calculate-with-offset(10px, 1px);
// Result: 11px - predictable
}
.element-2 {
width: calculate-with-offset(10px, 2px);
// Result: 12px - explicit and clear
}
Error Handling with @error and @warn
SASS provides @error and @warn directives to help catch bugs and provide helpful feedback during compilation.
Using @error for Critical Issues
// Function with error checking
@function safe-divide($dividend, $divisor) {
@if $divisor == 0 {
@error "Cannot divide by zero! Dividend: #{$dividend}";
}
@return $dividend / $divisor;
}
// This will compile
.valid {
width: safe-divide(100px, 2);
// Result: width: 50px;
}
// This will stop compilation with error message
.invalid {
width: safe-divide(100px, 0);
// ERROR: Cannot divide by zero! Dividend: 100px
}
// Validate function parameters
@function get-color($name) {
$colors: (
primary: #007bff,
secondary: #6c757d,
success: #28a745
);
@if not map-has-key($colors, $name) {
@error "Color '#{$name}' not found. Available colors: #{map-keys($colors)}";
}
@return map-get($colors, $name);
}
.button {
background: get-color(primary);
// Works fine
}
.bad-button {
background: get-color(danger);
// ERROR: Color 'danger' not found. Available colors: primary, secondary, success
}
Using @warn for Non-Critical Issues
// Function with warnings for deprecated usage
@function calculate-spacing($multiplier) {
@if $multiplier < 0 {
@warn "Negative spacing multiplier (#{$multiplier}) may cause layout issues.";
}
@if $multiplier > 10 {
@warn "Large spacing multiplier (#{$multiplier}) detected. Consider using a smaller value.";
}
@return $multiplier * 8px;
}
// These compile but show warnings
.element {
margin: calculate-spacing(-1);
// Warning: Negative spacing multiplier (-1) may cause layout issues.
// Result: margin: -8px;
}
.spacious {
margin: calculate-spacing(15);
// Warning: Large spacing multiplier (15) detected. Consider using a smaller value.
// Result: margin: 120px;
}
// Deprecation warnings
@function old-name($value) {
@warn "old-name() is deprecated. Use new-name() instead.";
@return new-name($value);
}
@function new-name($value) {
@return $value * 2;
}
Exercise 1: Unit Conversion Library
Create a comprehensive unit conversion library:
- Write a px-to-rem() function with a configurable base font size
- Write a rem-to-px() function that converts back to pixels
- Write a px-to-em() function that accepts a context parameter
- Write a strip-unit() function that removes the unit from a value
- Add @error handling for invalid inputs (negative values, unitless numbers where units expected)
- Create test cases using actual CSS classes to verify all functions work correctly
Exercise 2: Color System Builder
Build a complete color system using custom functions:
- Create a tint() function that lightens a color by mixing with white
- Create a shade() function that darkens a color by mixing with black
- Create an accessible-color() function that returns a WCAG-compliant text color for any background
- Create a color-palette() function that generates a map of 9 color variations (5 tints, base color, 4 shades)
- Use these functions to generate a complete set of color utilities (.bg-primary-1 through .bg-primary-9)
Exercise 3: Responsive Sizing System
Create a responsive sizing system with custom functions:
- Write a fluid-size() function that creates fluid typography using clamp()
- Write a responsive-spacing() function that calculates spacing based on viewport width
- Write a breakpoint-value() function that returns different values based on a breakpoint name
- Create a scale() function that generates sizes based on a modular scale ratio
- Use these functions to create a typography system (h1-h6) and spacing utilities (.m-1 through .m-5) that are fully responsive