SASS/SCSS

Custom Functions with @function

20 min Lesson 13 of 30

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%;
}
Note: Function names should be descriptive and use kebab-case (lowercase with hyphens) by convention. Unlike mixins, functions don't output CSS directly—they only return values that you can use in your CSS properties.

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
Tip: A good rule of thumb: if you find yourself wanting to use @return in a mixin, you probably need a function instead. If you want to output CSS rules, use a mixin.

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
}
Warning: Avoid creating functions that modify global variables or have side effects. Functions should be deterministic—given the same inputs, they should always return the same outputs. This makes your code predictable and easier to debug.

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;
}
Tip: Use @error for situations that will definitely cause problems (invalid inputs, missing required data). Use @warn for situations that might cause issues or for deprecation notices. This helps catch bugs during development without being overly strict.

Exercise 1: Unit Conversion Library

Create a comprehensive unit conversion library:

  1. Write a px-to-rem() function with a configurable base font size
  2. Write a rem-to-px() function that converts back to pixels
  3. Write a px-to-em() function that accepts a context parameter
  4. Write a strip-unit() function that removes the unit from a value
  5. Add @error handling for invalid inputs (negative values, unitless numbers where units expected)
  6. 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:

  1. Create a tint() function that lightens a color by mixing with white
  2. Create a shade() function that darkens a color by mixing with black
  3. Create an accessible-color() function that returns a WCAG-compliant text color for any background
  4. Create a color-palette() function that generates a map of 9 color variations (5 tints, base color, 4 shades)
  5. 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:

  1. Write a fluid-size() function that creates fluid typography using clamp()
  2. Write a responsive-spacing() function that calculates spacing based on viewport width
  3. Write a breakpoint-value() function that returns different values based on a breakpoint name
  4. Create a scale() function that generates sizes based on a modular scale ratio
  5. Use these functions to create a typography system (h1-h6) and spacing utilities (.m-1 through .m-5) that are fully responsive
Note: Custom functions are one of the most powerful features in SASS. They allow you to create reusable, testable, and maintainable style systems. Always strive to make your functions pure, well-documented, and include appropriate error handling.