@adrienlcp/styles
A reset, a reduced-motion switch, default tokens, Sass mixins for self-hosted fonts, container queries, a breakpoint, focus rings, visually hidden text and a two-column spread, and a WCAG contrast check for colour tokens
Fonctionne avec
- TypeScript
- CSS
- Sasspeer facultatif
Installation
pnpm add @adrienlcp/stylesExports
Groupés par la section de la documentation qui les explique. Une ligne mène à sa 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
tokens1 export
accessibility3 exports
spread1 export
contrast2 exports
Documentation
Le README et les documents à côté, tels qu’écrits dans le dépôt.
PrésentationREADME.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.
tokensREADME.md
The tokens every app names the same way, so an app sets values, not names.
Include defaults first in the app's :root; what the app declares after it
wins.
@use '@adrienlcp/styles/tokens'
@layer tokens
:root
@include tokens.defaults
--measure: 62ch| Token | Default |
|---|---|
--stroke-hair, --stroke-thin, --stroke-bold |
1px, 1.5px, 2px |
--hairline, --hairline-strong |
a --stroke-hair solid line in --rule, --rule-strong |
--inset-hairline, --inset-hairline-strong |
the same line as an inset box-shadow, which takes no room |
--outline-thick, --outline-offset |
2px, 3px |
--ring, --ring-offset, --ring-inset |
an --outline-thick solid outline in --focus, drawn --outline-offset outside the box or inside it |
--underline-offset, --tracking-tight |
0.24em, -0.02em |
--target, --control-touch |
44px, the smallest touch target |
--measure |
65ch |
--rule, --rule-strong and --focus are the app's palette. Until it declares
them, a line is currentColor mixed toward transparent and the ring is
currentColor: never invisible.
accessibilityREADME.md
@use '@adrienlcp/styles/accessibility'
.icon-label
@include accessibility.visually-hidden
.button
@include accessibility.ringvisually-hiddenhides from sight, not from a screen reader.ringdraws--ringon keyboard focus only::focus-visiblefor a native element,[data-focus-visible]for one react-aria marks.ring-insetdraws it inside the box, for a row that fills its container edge to edge or sits in an ancestor that clips. An app on@adrienlcp/react-ariahas the same pair in itsfocusmodule.
spreadREADME.md
A page of two columns: .columns holding two .columns, stacked and ruled
apart under breakpoints.wide, side by side above it with the rule running the
full height between them.
@use '@adrienlcp/styles/spread'
.settings-page
@include spread.columns($gap: var(--space-m))$gap spaces a column's own items; $rule (--hairline), $stacked-gap
(--space-l) and $spread-gap (--space-2xl) 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.