@adrienlcp/styles
A reset, a reduced-motion switch, Sass mixins for self-hosted fonts, container queries and a breakpoint, and a WCAG contrast check for colour tokens
Works with
- TypeScript
- CSS
- Sassoptional peer
Install
pnpm add @adrienlcp/stylesExports
Grouped by the section of the documentation that explains them. A row leads to its section.
Plain CSS2 exports
containers3 exports
containerMakes the element a container its descendants query by inline size.@adrienlcp/Sassstyles/ containers container-wideMatches when the nearest container — or the one named — is at least$widthwide.@adrienlcp/Sassstyles/ containers container-narrowThe exact complement ofcontainer-widefor the same$widthand$name.@adrienlcp/Sassstyles/ containers
breakpoints3 exports
fonts3 exports
contrast2 exports
Documentation
The README and the documents beside it, as written in the repository.
OverviewREADME.md
The styling floor every app of mine starts from: a reset, a reduced-motion
switch, and the Sass mixins that CSS cannot write yet. Plain .css for what
CSS can say, .sass only for what it cannot — a custom property is not allowed
in a media query or in unicode-range, and CSS has no mixins.
pnpm add @adrienlcp/stylesPlain CSSREADME.md
| File | Does |
|---|---|
reset.css |
Box sizing, zeroed margins and paddings, inherited fonts on controls, bare buttons and lists, balanced headings, pretty paragraphs, and interpolate-size: allow-keywords so a transition reaches height: auto (Chromium; elsewhere the size snaps as before). Inside @layer reset |
reduced-motion.css |
Collapses --transition-fast, --transition-base and --transition-slow to 0ms under prefers-reduced-motion: reduce, and stills view transitions, which React's <ViewTransition> starts whatever the preference. Unlayered, so it beats the tokens wherever they are defined |
Import them once — from JavaScript, or from the global stylesheet in Sass:
@use '@adrienlcp/styles/reset.css'
@use '@adrienlcp/styles/reduced-motion.css'reset.css declares a layer: set the order in the document head before any
stylesheet loads, so the reset stays under every component rule —
<style>@layer reset, tokens, base, components;</style>.
SassREADME.md
Resolved through the sass export condition, which Vite reads; with the Sass
CLI, use pkg:@adrienlcp/styles/breakpoints and --pkg-importer=node.
containersREADME.md
A component answers the room it is given, not the screen: the same card lays out the same way in a sidebar on a desktop and full width on a phone.
@use '@adrienlcp/styles/containers'
.card
@include containers.container
.card-body
@include containers.container-wide(30rem)
grid-template-columns: auto 1frcontainer($name: null)setscontainer-type: inline-size, andcontainer-namewhen given one.container-wide($width, $name: null)iswidth >= $widthon the nearest container, or on the one named;container-narrowis its exact complement.- An element queries an ancestor, never itself — the container wraps what changes.
- A container no longer takes its width from its content: inside a flex row
without a width, an
autogrid track or an absolutely positioned box, it collapses to zero. Its parent gives it a width. - With no container above, neither mixin matches. Make
bodyone in the global stylesheet, so an unwrapped component falls back to the page width;position: fixedstays on the viewport. - The width is Sass interpolated into
@container, so pass a value, not a custom property —@containercannot read one either.
breakpointsREADME.md
For what depends on the device rather than the room a component has: the page shell, a sidebar that turns into a drawer, an overlay pinned to the viewport.
@use '@adrienlcp/styles/breakpoints'
.shell
@include breakpoints.wide
grid-template-columns: 16rem 1fr$wide-screen is 900px; wide is width >= $wide-screen and narrow its
exact complement. Another value: @use '@adrienlcp/styles/breakpoints' with ($wide-screen: 1024px).
fontsREADME.md
$latin and $latin-ext are the unicode ranges Google Fonts cuts a Latin face
into; font-face declares one self-hosted woff2 file.
@use '@adrienlcp/styles/fonts'
@include fonts.font-face('Archivo', '/fonts/archivo-latin.woff2', fonts.$latin, $weight: 100 900, $stretch: 62% 125%)
@include fonts.font-face('Archivo', '/fonts/archivo-latin-ext.woff2', fonts.$latin-ext, $weight: 100 900, $stretch: 62% 125%)$weight, $style (normal), $stretch (left out) and $display (swap)
are optional.
TypeScriptREADME.md
contrastREADME.md
A palette is checked where it is written, not by eye: a test lists the token pairs that meet on screen, and fails when one falls under its WCAG minimum in the light or the dark scheme.
import { readFileSync } from 'node:fs'
import { findContrastFailures, WCAG_AA } from '@adrienlcp/styles/contrast'
import { expect, it } from 'vitest'
const TOKENS = readFileSync(new URL('_tokens.sass', import.meta.url), 'utf8')
it('every ink reads on every surface, in both themes', () => {
expect(
findContrastFailures(TOKENS, [
{ foreground: '--ink-soft', background: '--ground', minimum: WCAG_AA.text },
{ foreground: '--focus', background: '--ground', minimum: WCAG_AA.nonText }
])
).toEqual([])
})WCAG_AAistext(4.5),largeTextandnonText(3) — controls, icons, focus rings.- The stylesheet is
.cssor indented.sass. A token isoklch()or hex,light-dark()of those, orvar()of another token. - A failure is
too-low, with the ratio and the schemes it fails in, orunreadable: a token undeclared, declared twice with different values, a reference loop, a colour it cannot read (computed, or another notation), or a translucent background — what shows through decides that contrast. - A translucent foreground is measured over its background. An
oklch()outside sRGB is clipped, as an sRGB screen draws it.