A while back I, here on SitePoint, wrote Managing Responsive Breakpoints with Sass, presenting a couple of ways to deal with responsive design breakpoints using variables and mixins.
The last solution I introduced relies on a map of breakpoints, usually stored in the configuration file and a mixin taking a breakpoint name as the only argument, outputting a media query block. It should look like this:
// In `_config.scss`
$breakpoints: (
'small' : 767px,
'medium' : 992px,
'large' : 1200px,
) !default;
// In `_mixins.scss`
@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
}
@else {
@warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. "
+ "Please make sure it is defined in `$breakpoints` map.";
}
}
// In `_random-component.scss`
.foo {
color: red;
@include respond-to('small') {
color: blue;
}
}
This solution is perfectly fine for many projects, from small to medium scale. Yet, when we start to have many components, it tends to fall a bit short.
Indeed, we can make a distinction between two different kinds of breakpoints:
- actual breakpoints, involving layout recomposition;
- component-specific breakpoints, helping polishing the look and feel of the page.
Layout related breakpoints are basically what we have in our previous example. They dictate when there is a major change in the layout, for instance when a column is added or removed from the grid. They allow the layout to change in order not to look broken, hence the name breakpoints.
But more often than not, layout breakpoints are not enough. A components behavior should not be dictated by them. Each specific component should have its own set of breakpoints, triggering micro-layout changes inside of this specific component. Those breakpoints are not actually breakpoints since layouts do not break without them, thus should be called tweakpoints, as suggested by Jeremy Keith in Tweakpoints.
In my experience, not all breakpoints are created equal. Sure, there are the points at which the layout needs to change drastically in order for the content not to look like crap—those media queries can legitimately be called breakpoints. But then there are the media queries that are used to finesse page elements without making any major changes to the layout.
[…]
It feels a bit odd to call them breakpoints, as though the layout would “break” without them. Those media queries are there to tweak the layout. They’re not breakpoints; they’re tweakpoints.
– From Jeremy Keith in Tweakpoints
The two are not mutually exclusive of course. Au contraire! You’ll often find yourself wanting to join major structure changes to your components on layout breakpoints if needed, and apply micro optimizations on component-specific tweakpoints.
In this article, we will overload our existing system to make it possible to deal with both application-wide breakpoints and component breakpoints.
What do we need?
My idea was to keep application-wide breakpoints in the configuration file (_config.scss
or _variable.scss
), but to also have a local tweakpoints map in each component file. For instance, let’s consider a logo, living in components/_logo.scss
. To accommodate design changes, the logo happens to have two variations: an inline version, and a block version.
/// Logo-specific tweakpoints
/// @type Map
$tweakpoints: (
'inline' : 650px,
'block' : 980px,
);
.logo { .. }
.logo__image { .. }
.logo__baseline { .. }
Inside _logo.scss
, we have defined a $tweakpoints
map that contains breakpoints (well, tweakpoints but you get the idea at this point) which are specific to this logo component. They only make sense in this context thus should only be available in this file. That being said, global breakpoints should also be available in case we actually need them for a major change or anything.
So what do we need? We only need to make our mixin aware of this map. It should first check if the requested breakpoint lives in the local map, then if not try on the global map.
Overloading the mixin
Surprisingly enough, our mixin is actually very simple and not much longer that the previous one:
/// Breakpoints/tweakpoints manager
/// @param {String} $point - Breakpoint/tweakpoint name
@mixin respond-to($point) {
@if map-has-key($tweakpoints, $point) {
@media (min-width: map-get($tweakpoints, $point)) {
@content;
}
} @else if map-has-key($breakpoints, $point) {
@media (min-width: map-get($breakpoints, $point)) {
@content;
}
} @else {
@warn "Could not find `#{$breakpoint}` in both local ($tweakpoints) and global ($breakpoints) contexts. Media block omitted.";
}
}
There are three logical steps involved here:
- If the requested point exists in
$tweakpoints
, open a media query and leave the mixin. - If the requested point does not exist in
$tweakpoints
but exists in$breakpoints
, open a media query and leave the mixin. - If the requested point does not exist in either
$tweakpoints
or$breakpoints
, warn the user.
Note that if a breakpoint is present in both $tweakpoints
and $breakpoints
maps, the tweakpoint will override the breakpoint. For instance, if you have a medium
tweakpoint and a medium
breakpoint defined, the tweakpoint will be used when including respond-to('medium')
. This is intended if we ever want to locally override a breakpoint for the current component (although this is not something I would recommend).
Using it
Using this fresh mixin is not any different than using our previous one. The API has not changed much, and every change would be fully backward compatible, which is very nice in my opinion.
<a href="/" class="logo">
<img class="logo__image" src="..." alt="..." />
<p class="logo__baseline">...</p>
</a>
/// Logo-specific tweakpoints
/// @type Map
$tweakpoints: (
'inline' : 650px,
'block' : 980px,
);
/**
* Logo wrapper
* 1. Turn the link into a block level element
* 2. Size it based on the narrowest child when in *block* format
*/
.logo {
display: block; /* 1 */
padding: .5em;
color: currentcolor;
text-decoration: none;
@include respond-to('block') {
width: min-content; /* 2 */
margin: 1em auto;
text-align: center;
}
}
/**
* Logo image
* 1. Give it a max-width when inline'd so it does break out
*/
.logo__image {
vertical-align: middle;
@include respond-to('inline') {
max-width: 3em; /* 1 */
}
@include respond-to('block') {
display: block;
height: auto;
margin: 0 auto .5em;
}
}
/**
* Logo baseline
* 1. Inline it when in *inline* format
*/
.logo__baseline {
margin: 0;
vertical-align: middle;
@include respond-to('inline') {
display: inline-block; /* 1 */
}
}
Going further
Ideally, you want to reinitialize the $tweakpoints
map at the end of the component file so it does not leak to the next partial. Something as simple as this is enough:
$tweakpoints: ();
This makes sure the next component does not have any tweakpoint configured. Problem is that it is very easy to forget this line, which is actually not that of a big deal if next component defines a $tweakpoints
map itself. But if it does not, and the previous component overrides a global breakpoint with a tweakpoint, the next component will have the overridden breakpoint, not the global one.
For instance, consider this:
// In `_config.scss`
$breakpoints: (
'small': 767px,
'medium': 992px,
);
// In `_component-1.scss`
$tweakpoints: (
'custom': 500px,
'medium': 1170px;
);
// A lot of Sass rules here, but no `$tweakpoints: ();` at the end of file
// In `_component-2.scss`
// No `$tweakpoints` map defined for this component
// In `main.scss`
@import "utils/config";
@import "components/component-1";
@import "components/component-2";
Now, if we happen to have @include respond-to('medium')
in _component-2.scss
, the breakpoint’s value will be 1170px
, not 992px
. This happens because the previous component (_component-1.scss
) overrides the global medium
with a local value, and does not reset the tweakpoints map at the end of file.
Okay, so this sucks and is very error prone. We could surely do better!
Indeed we can. What if we had a component
mixin that takes care of assigning and reseting $tweakpoints
? It is basically a wrapper for our component.
/// Component wrapper
/// @param {Map} $component-tweakpoints [()] - Component tweakpoints
@mixin component($component-tweakpoints: ()) {
$tweakpoints: $component-tweakpoints !global;
@content;
$tweakpoints: () !global;
}
That’s all we need. Now, let’s rewrite our previous example:
// In `_config.scss`
$breakpoints: (
'small': 767px,
'medium': 992px,
);
// In `_component-1.scss`
@include component((
'custom': 500px,
'medium': 1170px,
)) {
// A lot of Sass rules here, but no `$tweakpoints: ();` at the end of file
}
// In `_component-2.scss`
@include component {
// No `$tweakpoints` map defined for this component
}
// In `main.scss`
@import "utils/config";
@import "components/component-1";
@import "components/component-2";
That’s it! No more issue because the component
mixin took care of resetting the $tweakpoints
map at the end of the component, making sure there is no unfortunate leak.
Final thoughts
To be perfectly honest with you guys, I have not tested this solution in a real life project just yet, but I do think it is a valuable extra feature to breakpoint management. Thanks to this little addition, it is getting very easy to handle both application-wide breakpoints and component-specific tweakpoints.
I should probably warn you about a potential drawback of this method: wrapping each component in a mixin call (@include component(..) { .. }
) prevents you from declaring component-specific mixins, since you cannot define a mixin within a mixin call. That being said, I am not a big fan of having component-specific mixins and functions, so it might not be an actual drawback if you happen to think like me.
I would like to finish this article by mentioning that Sass-MQ from Kaelig allows something equivalent.
// _random-component.scss
$tweakpoints: (
'custom': 500px,
'medium': 1170px,
);
.foo {
@include mq('medium') {
color: red;
}
@include mq('medium', $breakpoints: $tweakpoints) {
color: blue;
}
}
As you can see, the mq
mixin accepts a breakpoints map as an extra argument, which defaults to $mq-breakpoints
(the map used by Sass-MQ to store global breakpoints). If you want to make the API slightly more convenient, you can always write your own little mixin:
@mixin component-mq($from: false, $until: false) {
@include mq($from: $from, $until: $until, $breakpoints: $tweakpoints) {
@content;
}
}
Then you use it like this:
.foo {
// Global medium: 992px
@include mq('medium') {
color: red;
}
// Local medium: 1170px
@include component-mq('medium') {
color: blue;
}
}
That’s it! I hope you like it, and if you have any suggestion be sure to share in the comments! :)
Frequently Asked Questions (FAQs) about Breakpoints and Tweakpoints in Sass
What is the difference between breakpoints and tweakpoints in Sass?
Breakpoints and tweakpoints are both used in Sass to create responsive designs. However, they serve different purposes. Breakpoints are used to change the layout of a website at different screen sizes. They are typically used to switch between different layouts for mobile, tablet, and desktop screens. On the other hand, tweakpoints are used to make minor adjustments to the design at specific screen sizes. These adjustments could include changing the font size, adjusting the padding, or tweaking the margin. Tweakpoints are typically used to fine-tune the design at different screen sizes, rather than changing the entire layout.
How do I set up breakpoints in Sass?
Setting up breakpoints in Sass is straightforward. You can define your breakpoints using variables, and then use these variables in your media queries. Here’s an example:$breakpoint-mobile: 480px;
$breakpoint-tablet: 768px;
$breakpoint-desktop: 1024px;
@media (min-width: $breakpoint-mobile) {
// Mobile styles
}
@media (min-width: $breakpoint-tablet) {
// Tablet styles
}
@media (min-width: $breakpoint-desktop) {
// Desktop styles
}
In this example, we’ve defined three breakpoints for mobile, tablet, and desktop screens. We then use these breakpoints in our media queries to apply different styles at different screen sizes.
How can I use tweakpoints in Sass?
Tweakpoints in Sass are used in a similar way to breakpoints. You define your tweakpoints using variables, and then use these variables in your media queries. However, tweakpoints are typically used to make minor adjustments to the design, rather than changing the entire layout. Here’s an example:$tweakpoint-small: 320px;
$tweakpoint-medium: 640px;
@media (min-width: $tweakpoint-small) {
// Small screen tweaks
}
@media (min-width: $tweakpoint-medium) {
// Medium screen tweaks
}
In this example, we’ve defined two tweakpoints for small and medium screens. We then use these tweakpoints in our media queries to make minor adjustments to the design at these screen sizes.
Can I use both breakpoints and tweakpoints in the same project?
Yes, you can use both breakpoints and tweakpoints in the same project. In fact, it’s common to use both in a responsive design. You would use breakpoints to switch between different layouts for mobile, tablet, and desktop screens, and then use tweakpoints to fine-tune the design at specific screen sizes.
What are the best practices for using breakpoints and tweakpoints in Sass?
There are several best practices for using breakpoints and tweakpoints in Sass. First, it’s important to define your breakpoints and tweakpoints using variables. This makes it easy to update your breakpoints and tweakpoints in one place, rather than having to update them in multiple places throughout your code. Second, it’s a good idea to use min-width in your media queries, rather than max-width. This is because min-width is more future-proof, as it allows your design to scale up to larger screen sizes. Finally, it’s important to test your design at different screen sizes to ensure that your breakpoints and tweakpoints are working as expected.
Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.