Breakpoints
A breakpoint is the viewport width where a layout stops working well one way and switches to another, going from a single stacked column on a phone to several columns side by side on a wider screen, for example. Reaching for one shared set of widths instead of a new number each time keeps every layout switch in an app landing on the same handful of points, so the whole page still feels consistent as it resizes.
Eunomia has four breakpoints, mobile, tablet, laptop and
desktop. Each one is the width where a level starts: tablet is
768px, so the tablet level applies from 768px up, and
everything below it stops just under. Reach for tablet when a layout
has enough room to stop behaving like a phone, the most common switch
in practice, and for mobile when a change only matters on the
smallest phones. laptop and desktop are there for a change that
only makes sense once a screen has even more room to spare.
Try it
Drag the box's bottom-right corner, or drag the cursor on the ruler
below. The two are connected, so moving one moves the other, and you
can also focus the cursor and use the arrow keys. Each breakpoint
lights up the moment the width reaches it, the same width >= test as
mediaFrom and the eunomia-from-* mixins.
One source for everything
The breakpoints are defined once, in EUNOMIA_BREAKPOINTS. Everything
else is built from that object: the components' own media and container
queries, the Sass scale behind the mixins and the grid classes, and the
--eun-breakpoint-* custom properties. The library's lint fails if
any of them drifts from it, or if a width is written by hand anywhere
in the library, so the values below are the ones every layout switch
actually uses.
| Breakpoint | Width | Below | From |
|---|---|---|---|
mobile |
600px |
(width < 600px) |
(width >= 600px) |
tablet |
768px |
(width < 768px) |
(width >= 768px) |
laptop |
1024px |
(width < 1024px) |
(width >= 1024px) |
desktop |
1280px |
(width < 1280px) |
(width >= 1280px) |
Range syntax
Every condition uses the range syntax, width < 768px below a
breakpoint and width >= 768px from it. At the breakpoint itself,
exactly one side applies, and there's no gap between them: the older
max-width: 767px / min-width: 768px pair leaves every width
between the two numbers unmatched, which a zoomed page or a fractional
pixel ratio can land on. The range syntax works in every current
browser. Write your own queries the same way, or let the helpers below
write them for you.
Using the breakpoints
In CSS
Once a theme stylesheet is loaded, the widths are also custom properties:
| Token | Value |
|---|---|
--eun-breakpoint-mobile |
600px |
--eun-breakpoint-tablet |
768px |
--eun-breakpoint-laptop |
1024px |
--eun-breakpoint-desktop |
1280px |
.custom-panel {
max-width: var(--eun-breakpoint-tablet);
}
A @media condition can't read a custom property, the CSS spec doesn't
allow it. Use these where a breakpoint is needed inside a regular
declaration (a max-width, a calc()) or read from JavaScript with
getComputedStyle. To switch a layout in plain CSS, write the
condition from the table at the top of this page, or use the Sass
mixins.
In Sass
eunomia-below-<breakpoint> applies below a breakpoint and
eunomia-from-<breakpoint> from it, with the same conditions as
mediaBelow and mediaFrom. eunomia-below($breakpoint) and
eunomia-from($breakpoint) take the name as an argument, and fail the
build on an unknown one. Load them with
@use "@eunomia/elements/styles/breakpoints" as *;, which also brings
the $map-device-width scale and emits no CSS of its own.
| Mixin | Applies |
|---|---|
eunomia-below-mobile |
(width < 600px) |
eunomia-below-tablet |
(width < 768px) |
eunomia-below-laptop |
(width < 1024px) |
eunomia-below-desktop |
(width < 1280px) |
eunomia-from-mobile |
(width >= 600px) |
eunomia-from-tablet |
(width >= 768px) |
eunomia-from-laptop |
(width >= 1024px) |
eunomia-from-desktop |
(width >= 1280px) |
@use "@eunomia/elements/styles/breakpoints" as *;
.my-panel {
@include eunomia-below-tablet {
flex-direction: column;
}
}
// Between two breakpoints : nest them
.my-sidebar {
@include eunomia-from-tablet {
@include eunomia-below-laptop {
display: none;
}
}
}
In JavaScript
mediaBelow and mediaFrom return the condition for a breakpoint,
ready for matchMedia, a @media rule or a @container rule. Import
them from the @eunomia/elements/breakpoints.js subpath, which
registers no component, so it's safe to load on a server or in a hook
without pulling in the whole library. EUNOMIA_BREAKPOINTS comes from
the same subpath when you need a raw width.
import { mediaBelow } from "@eunomia/elements/breakpoints.js";
const isMobile = window.matchMedia(mediaBelow("tablet"));
In a Lit css template, wrap the condition with unsafeCSS:
import { css, unsafeCSS } from "lit";
import { mediaBelow } from "@eunomia/elements/breakpoints.js";
css`
@media ${unsafeCSS(mediaBelow("tablet"))} {
:host {
flex-direction: column;
}
}
`;
Containers
Container queries
Every condition above reacts to the browser viewport. A @container
condition reacts to an ancestor's own rendered size instead, so a
component embedded in a narrow dashboard panel compacts the same way
it would in a narrow browser window, regardless of how wide the page
around it actually is. eun-calendar's hour grid uses exactly this to
size its own layout off its rendered width rather than the page's.
An element only becomes a query container once something declares it
one with container-type, and mediaBelow and mediaFrom plug into
the condition the same way they do in a @media rule:
import { css, unsafeCSS } from "lit";
import { mediaBelow } from "@eunomia/elements/breakpoints.js";
css`
:host {
container-type: inline-size;
}
@container ${unsafeCSS(mediaBelow("tablet"))} {
.content {
flex-direction: column;
}
}
`;
Give the container a container-name when a query needs to skip past
a nearer container and reach a specific named ancestor instead of the
closest one:
:host {
container-type: inline-size;
container-name: my-panel;
}
@container my-panel (width < 768px) {
.content {
flex-direction: column;
}
}
Container styles
A container query can also read a custom property's value instead of
a size, with style(). Combined with a size query that sets the
property, this turns a one-off width check into something the rest of
an app can also read, inherit, or override directly:
:host {
--my-panel-compact: false;
}
@container (width < 768px) {
:host {
--my-panel-compact: true;
}
}
@container style(--my-panel-compact: true) {
.content {
flex-direction: column;
}
}
Because --my-panel-compact is a real custom property, it inherits
like any other, so an app can force it on an ancestor
(.my-panel { --my-panel-compact: true; }) to opt into the compact
layout regardless of the container's actual rendered width. This is
exactly the pattern behind eun-stepper's --stepper-mobile and
eun-calendar's --scheduler-mobile/--scheduler-tablet custom
properties, see the "See also" links below for both in context.
See also
Space : the spacing scale used alongside these breakpoints inside componentsGrid utilities : page-level layout classes, whose column variables switch at these same breakpointsStepper : its "Compact display" section shows the container style-query flag pattern above on a real component, via--stepper-mobileCalendar : its hour grid sizes itself off its own rendered width with a@containersize query, via--scheduler-mobile/--scheduler-tablet