Last active
January 24, 2021 16:35
-
-
Save audunolsen/d594c06221b63d97ae5459e0e3f1e572 to your computer and use it in GitHub Desktop.
Responsive development SCSS JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
// … | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$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)}'; } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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