Skip to content

Instantly share code, notes, and snippets.

@audunolsen
Last active January 24, 2021 16:35
Show Gist options
  • Save audunolsen/d594c06221b63d97ae5459e0e3f1e572 to your computer and use it in GitHub Desktop.
Save audunolsen/d594c06221b63d97ae5459e0e3f1e572 to your computer and use it in GitHub Desktop.
Responsive development SCSS JS
interface BreakpointState {
[key: string]: boolean;
}
export class Breakpoints {
get all() {
const serialized = isBrowser() && getComputedStyle(document.body).getPropertyValue('--breakpoints');
let parsed;
try {
if (serialized === '') throw new Error('Empty string');
parsed = Object.fromEntries(
serialized
.trim()
.slice(1, -1)
.split(',')
.map((e) => e.split(' '))
);
for (const [name, treshold] of Object.entries(parsed)) {
if (Number.isNaN(parseInt(treshold as string, 10)))
throw new Error(`${name} has an invalid length value: ${treshold}`);
parsed[name.replace(/-./g, (x) => x[1].toUpperCase())] = parseInt(treshold as string, 10);
if (name.includes('-')) delete parsed[name];
}
} catch (e) {
console.error(
[
'Could not parse breakpoint data…',
'root level css variable "--breakpoints" expects string w/ following format:',
'<name> <length value>, <name> <length value>, ...\n',
`Recieved: ${serialized} (${typeof serialized})\n`,
].join('\n'),
e
);
} finally {
parsed = parsed || {};
}
return parsed;
}
get isGreater() {
const state: BreakpointState = {};
for (const [name, treshold] of Object.entries(this.all)) {
state[name] = innerWidth >= treshold;
}
return state;
}
get isLesser() {
const state: BreakpointState = {};
for (const [name, treshold] of Object.entries(this.all)) {
state[name] = innerWidth < treshold;
}
return state;
}
}
export const breakpoints = new Breakpoints();
body {
// Apply styles every other breakpoint
@include breakpoints(between x-small small, between medium large, from x-large) {
background: blue;
p {
color: red;
}
}
// No unit defualts to px
@include breakpoints(from 200) {
// …
}
@include breakpoints(to 500px) {
// …
}
}
import { useEffect } from 'react';
import useBreakpoints from './hooks/useBreakpoints';
function App() {
const breakpoints = useBreakpoints();
useEffect(() => {
console.log(breakpoints.all);
}, []);
useEffect(() => {
console.log('crossed breakpoint treshold');
}, [breakpoints]);
return (
<div>
{breakpoints.isLesser?.small && <h3>Viewport short of "small" breakpoint</h3>}
{(breakpoints.isGreater?.small && breakpoints.isLesser?.large) && (
<h2>Viewport between "small" and "large" breakpoints</h2>
)}
{breakpoints.isGreater?.large && <h1>Viewport exceeds "large" breakpoint</h1>}
<p>Breakpoints demo</p>
</div>
);
}
export default App;
$breakpoints: (
x-small: 375,
small: 768,
medium: 1024,
large: 1440,
x-large: 1920,
);
@function list-get($list, $index) {
// Needed because nth return fatal error if not found
@return if(length($list) >= $index, nth($list, $index), null);
}
@function coerceTreshold($val) {
$val: map-get($breakpoints, $val) or $val;
@if $val == null {
@return
'No treshold recieved (null). ' +
'Treshold must be an existing breakpoint map key or a length value.';
}
@if type-of($val) == string {
@return 'breakpoints map does not contain "#{$val}"';
}
@if type-of($val) != number {
@return '#{$val} is not a valid length value';
}
@return if(unit($val) != '', $val, $val + 0px);
}
/* ————————————————————————————————————————————————————
Breakpoint mixin sporting more succinct and
intuitive syntax for creating @media query rules
USAGE:
@include breakpoints(
<to | from | between> <key in breakpoints map or length value (two if between)>,
<... (allows for multiple ranges)>
) { @content }
EXAMPLE:
body {
@include breakpoints(to 200, between medium x-large) {
background: red;
}
}
———————————————————————————————————————————————————— */
@mixin breakpoints($ranges...) {
$rule: 'screen and ';
// 1. argument validation
@if length($ranges) < 1 {
@error 'Mixin requires at least one breakpoint/range';
}
@each $range in $ranges {
$i: index($ranges, $range);
@if type-of($range) != list {
@error 'Each argument must be a list. Recieved "#{$range}"';
}
$behaviour: list-get($range, 1);
@if not index((from between to), $behaviour) {
@error
'first list item must be one of: from | bewteen | to. ' +
'Recieved: #{$behaviour}';
}
@if length($range) != if($behaviour == between, 3, 2) {
@error
'"#{$behaviour}" only expects #{if($behaviour == between, 'two tresholds', 'one treshold')} ' +
'Recieved #{length($range) - 1}: "#{$range}"';
}
$treshold1: coerceTreshold(list-get($range, 2));
$treshold2: coerceTreshold(list-get($range, 3));
@if index((from to), $behaviour) {
@if type-of($treshold1) == string {
@error
'Could not create "#{$behaviour}" breakpoint: ' +
$treshold1;
}
}
@if $behaviour == between {
$tresholds: (
'from' : $treshold1,
'to' : $treshold2
);
@each $behaviour, $treshold in $tresholds {
@if type-of($treshold) == string {
@error
'Could not create "between" range. "#{$behaviour}": ' +
$treshold;
}
}
@if ($treshold1 > $treshold2) {
@error
'Could not create "between" range. ' +
'"from" (#{$treshold1}) cannot be larger than "to" (#{$treshold2})';
}
}
// 1. Create quries
@if $behaviour == between {
$rule: $rule + '(min-width: #{$treshold1}) and (max-width: #{$treshold2 - 1})';
} @else {
$dec: if($behaviour == from, 0, -1);
$rule: $rule +
'(#{if($behaviour == from, min, max)}-width: ' +
'#{coerceTreshold(nth($range, 2)) + $dec})';
}
@if $i != length($ranges) {
$rule: $rule + ', ';
}
}
@media #{$rule} { @content; }
}
/* ———————————————————————————————————————————
Create JS access points for breakpoints.
parsed by custom Breakpoints class.
@include set-breakpoint-data();
——————————————————————————————————————————— */
@function serialize-map($map) {
$str: '';
@each $key, $val in $map {
$index: index($breakpoints, $key $val);
$length: length($breakpoints);
$str: '#{$str}#{$key} #{$val}#{if($index != $length, ',', '')}';
}
@return $str;
}
@mixin set-breakpoint-data {
:root { --breakpoints: '#{serialize-map($breakpoints)}'; }
}
import { useEffect, useState } from 'react';
import { breakpoints, Breakpoints } from 'src/helpers/breakpoints';
import throttle from 'lodash.throttle';
const useBreakpoints = () => {
const setBreakpointState = useState('')[1];
useEffect(() => {
const throttledResize = throttle(() => {
setBreakpointState(JSON.stringify(breakpoints.isLesser) + JSON.stringify(breakpoints.isGreater));
}, 400);
function handleResize() {
throttledResize();
}
addEventListener('resize', handleResize);
return () => removeEventListener('resize', handleResize);
}, []);
return new Breakpoints();
};
export default useBreakpoints;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment