TypeScript helpers for developers writing custom CSS additions for Hanna-based designs.
It provides a convenient helper objects for using hanna's CSS variables design tokens, and several useful functions (a.k.a. mixins) to boot.
Developers using CSS-in-JS libraries, such as styled-components or emotion, will find this library quite useful.
yarn add @reykjavik/hanna-css
Table of Contents:
- Why TypeScript Instead of SASS?
- Generic CSS helpers
- Hanna CSS Variables
- Class-Name constants
- Helpful Constants
- Hanna CSS Env
- Accessibility Helpers
- Markup Warning Helpers
- Scaling Helpers
- Raw Design Constants
- Helpful VSCode Snippets
- Changelog
TL;DR: TypeScript provides better developer ergonomics – both internally in this monorepo and outside of it, and is a more future-proof technology than SASS.
SASS has been almost an industry standard tool for templating CSS code for well over a decade now. Yet it provides poor developer experience with lackluster editor integrations, idiosyncratic syntax, extremely limited feature set, it's hard to publish and consume libraries, etc…
The web development community has been steadily moving on to other, more nimble technologies — either more vanilla "text/css" authoring, or class-name-based reverse compilers like Tailwind, or various JavaScript-based solutions (including literal CSS-in-JS).
This package provides supportive tooling for this last group, but offers also a new lightweight alternative: To author CSS using JavaScript as a templating engine, but then output it via one of the following methods:
- Simple
writeFile
ing the string result to static file - Use something like the es-to-css compiler,
- Or stream it directly to the browser.
However, if SASS remains your thing you could still use this library to
programmatically generate some key *.scss files with SASS variables, etc. and
then @use
those in the SASS files you write. You do you. ❤️
For convenience, @reykjavik/hanna-css
re-exports all types and helper
methods from the
es-in-css
library (excluding the
JS-to-CSS "compiler").
Please refer to the
es-in-css
documentation for more
info.
For the best developer experience, use VSCode and install the official
vscode-styled-components extension.
That gives you instant syntax highlighting and IntelliSense autocompletion
inside css``
template literals.
You might also want to add a couple of hanna-css VSCode "snippets".
The Hanna design system's CSS relies heavily on CSS custom properties (a.k.a. "CSS variables") for storing its most low-level design token values, and this package provides easy access to those variables, and helpers for generating your own.
The values of those variables are declared as part of the
Hanna -basics
CSS.
Syntax: hannaVars: Record<HannaCssVarToken, VariablePrinter>
Type-safe collection of CSS variables for use in your CSS code.
import { hannaVars, css } from '@reykjavik/hanna-css';
const myCss = css`
.SomeComponent {
background-color: ${hannaVars.theme_color_primary};
font: ${hannaVars.font_hd_s};
max-width: ${hannaVars.grid_6};
}
`;
/*`
.SomeComponent {
background-color: var(--theme-color-primary);
font: var(--font-hd-s);
max-width: var(--grid-4);
}
`*/
Syntax: See
es-in-css
— VariableStyles.override
This function provides a type-safe way to write local overrides for the Hanna CSS variables. Use sparingly, with caution!
import { hannaVarOverride, css } from '@reykjavik/hanna-css';
const myCss = css`
.SomeComponent {
${hannaVarOverride({
color_faxafloi_100: `red`,
})}
}
`;
/*`
.SomeComponent {
--color-faxafloi-100: red;
}
`*/
Syntax: formFieldVars: Record<string, VariablePrinter>
Type-safe collection of CSS variables for styling FormField
-derived
components.
import { formFieldVars, css } from '@reykjavik/hanna-css';
const myCss = css`
.MyFormField__input__dohicky {
background-color: ${formFieldVars.input__background_color};
...other dohicky styling..
}
`;
/*`
.MyFormField__input__dohicky {
background-color: var(--FormField-input--background-color);
...other dohicky styling...
}
`*/
Syntax:
buildVariables<T extends string>(vars: Array<T>, namespace?: string): VariableStyles<T>
A limited, pre-configured version of the
makeVariables
helper
from es-in-css
.
You can use this helper to generate custom CSS variables for your one-off
component styling, using the same naming pattern as the Hanna CSS varibles,
and the same type-safety as hannaVars
.
import { buildVariables, rem, css } from '@reykjavik/hanna-css';
const myVars = buildVariables(['Component$$title__fontSize']);
const myCss = css`
.Component {
${myVars.declare({ Component$$title__fontSize: rem(2) })}
}
.Component__title {
font-size: ${myVars.vars.Component$$title__fontSize};
}
`;
/*`
.Component {
--Component__title--fontSize: 2rem;
}
.Component__title {
font-size: var(--Component__title--fontSize);
}
`*/
The optional namespace
parameter gets prepended to the generated CSS
variable names. (NOTE: Namespaces are internally normalized to end with either
--
or __
.)
Thus the code example above could be rewritten like this:
const namespace = 'Component__';
const myVars = buildVariables(['title__fontSize'], namespace);
const myCss = css`
.Component {
${myVars.declare({ title__fontSize: rem(2) })}
}
.Component__title {
font-size: ${myVars.vars.title__fontSize};
}
`;
/*`
.Component {
--Component__title--fontSize: 2rem;
}
.Component__title {
font-size: var(--Component__title--fontSize);
}
`*/
Collection of selectors with class-name-states that the <html/>
element can
take.
Here's how you'd use the beforeSprinkling
selector to suppress flicker of
unstyled/unscripted content in your server-rendered HTML, while you
"progressively enhance" them after useEffect.
import { htmlCl, css } from '@reykjavik/hanna-css';
const myCss = css`
${htmlCl.beforeSprinkling} .MyComponent__details:not([data-sprinkled]) {
display: none;
}
.MyComponent__details[data-sprinkled] {
opacity: 0;
height: 0;
}
`;
/*`
.before-sprinkling .MyComponent__details:not([data-sprinkled]) {
display: none;
}
.MyComponent__details[data-sprinkled] {
opacity: 0;
height: 0;
}
`*/
Other selectors: menuIsActive
, menuIsOpen
, menuIsClosed
Each of these properties has JSDoc comments associated with them. Refer to those for more info.
Object containing the names of the Hanna color themes.
import { colorThemes } from '@reykjavik/hanna-css';
import type { HannaColorTheme } from '@reykjavik/hanna-css';
const themeName: HannaColorTheme = colorThemes.trustworthy;
console.log(themeName);
// "trustworthy"
Object containing the names of the Hanna base color families.
import { colorFamilies } from '@reykjavik/hanna-css';
import type { ColorFamily } from '@reykjavik/hanna-css';
const familyName: ColorFamily = colorFamilies.esja;
console.log(familyName);
// "esja"
Object with several named unicode symbols for use in generated content
(::marker
s, ::before
texts, etc.). Includes bullets
, spaces
, quotes
import { characters } from '@reykjavik/hanna-css';
const { bullets, spaces, quotes } = characters;
console.log(quotes.IS.open + 'Hæ!' + quotes.IS.close);
// "„Hæ!“"
Object with the names of the "decorative" icons available for general use with
data-icon=""
and data-icon-after=""
attributes, and for React component
icon props.
import { icons } from '@reykjavik/hanna-css';
import type { IconName } from '@reykjavik/hanna-css';
const iconName: IconName = icons.close;
console.log(iconName);
// "close"
The mq
object contains pre-fabricated media-query strings for the basic
Hanna media/screen types.
These are:
-
phone
: Typical mobile phone in portrait mode (~ 320—480px wide) -
phablet
: Mobile phones in landscape mode and tiny tablets (~ 480—760px wide) -
tablet
: Typical tablets like iPads (not pro), in portrait mode (~ 760—980px wide) -
netbook
: Tiny laptops, large tablets and smaller iPads in landscape mode (~ 980—1368px wide) -
wide
: Desktops and larger laptops (~ 1368px wide and up)
It also contains queries that (inclusively) span multiple media/screen
types:
netbook_up
, tablet_netbook
, tablet_up
, phablet_tablet
,
phablet_netbook
, phablet_up
, phone_phablet
, phone_tablet
,
phone_netbook
.
import { mq, css } from '@reykjavik/hanna-css';
const myCss = css`
@media ${mq.tablet_up} {
.SomeComponent {
width: 100%;
}
}
`;
/*`
@media (min-width: 760px) {
.SomeComponent {
width: 100%;
}
}
`*/
Syntax:
getCssBundleUrl(cssTokens: string | Array<CssModuleToken>, options?: CssBundleOpts): string
This methods generates a URL to load a correctly versioned CSS bundle from the Hanna Style Server.
You must pass a list of cssTokens
corresponding to the Hanna design
components you use on your page(s).
import { getCssBundleUrl } from '@reykjavik/hanna-css';
const cssTokens = [
'-basics', // The required base style reset
'Layout',
'HeroBlock',
'TextInput',
'Selectbox',
'ButtonPrimary',
// etc…
];
const cssUrl = getCssBundleUrl(cssTokens);
NOTE: You need to remember to explicitly include the -basics
token — unless
you're incrementally adding single CSS tokens after the fact.
However, because of how CSS cascade works, it's strongly recommended to
try and maintain (and update in-place) a single CSS
<link rel="stylesheet" href="..." />
URL, instead of requesting multiple CSS
bundles. (NOTE: If you're using the @reykjavik/hanna-react
package you
should use it's <HannaCssLink ... />
component.)
Multiple bundles will often work, but may occasionally fail in unpredictable ways.
CssBundleOpts.version?: CssVersionToken
The default is always the most recent major version of the Hanna CSS files.
Use this option if you, for some reason, wish/need to pin your CSS files to an older, or more specific version folder.
const cssUrl = getCssBundleUrl(cssTokens, { version: 'v1.0.3' });
TypeScript Note: The version
option is by default type-restricted to the
CSS versions known at the time when the library was published. If you need to
set a newer (minor/patch) CSS version, you should ideally update hanna-css
for an updated version list. If that's not possible, you can pass a type-level
generics paramter of true
which relaxes the type restrictions a bit.
getCssBundleUrl<true>(cssTokens, { version: 'v1.13' });
/* Like this ———^^^^ */
All the currently known CSS module tokens that are available on the
Hanna Style Server at
the time when the library was published. If you need to load newer tokens, you
should ideally update hanna-css
for an updated token list. If that's not
possible, you can convert your token Array to comma-delimited string.
Syntax: getEssentialHannaScripts: () => string
Essential Hanna styling assisting scripts. These provide flicker-free progressive enhancement for server-rendered dynamic Hanna UI compoennts, and fix some Safari-related styling issues.
Inline this script snippet as close to the top of your page's <head/>
element as you can.
Syntax: styleServerUrl: string
Re-export from
@reykjavik/hanna-utils/assets
.
This URL is useful when building links linking to assets, etc, and is used
internally by getCssBundleUrl()
Syntax: setStyleServerUrl: (url: string | URL) => void
Re-export from
@reykjavik/hanna-utils/assets
.
This updates the value of styleServerUrl
globally. Use it at the top of your
application if you want to load assets and CSS bundles from a custom
style-server instance, e.g. during testing/staging/etc.
Syntax: targetCssVersion: string
The current MAJOR version of the Hanna style-server CSS files this version of
@reyjkjavik/hanna-css
package targets.
Primary use is for debugging/informational purposes.
Syntax: isDevMode: boolean
Convenience shorthand for process.env.NODE_ENV !== 'production'
, used
internally in some of the exported mixins, etc.
import { isDevMode } from '@reykjavik/hanna-css';
const myCss = css`
.SomeComponent {
color: ${isDevMode ? 'red' : 'blue'};
}
`;
Syntax srOnly: () => CssString
Mixin that hides an element visually, but still makes it accessible to screen readers.
import { css, srOnly } from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent__label {
${srOnly};
}
`;
In the rare cases where you might need to make a sr-only element visible
again, there's srOnly__undo
that you can import and apply. (This can usually
be avoided by using more precise selectors for the srOnly
mixin.)
Syntax srOnly_focusable: () => CssString
Similar to the srOnly
mixin, but intended for links/buttons that should
become visible on keyboard focus (:focus-visible
).
import { css, srOnly_focusable } from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent__skipLink {
${srOnly_focusable};
}
`;
Syntax srOnly_focusableContent: () => CssString
Similar to the srOnly_focusable
mixin above, but for non-interactive
elements that contain buttons/links that should become visible on keyboard
focus.
import { css, srOnly_focusableContent } from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent__dragdrop-controls {
${srOnly_focusableContent};
}
`;
Syntax:
hoverKeyboardFocusAndActiveStyling: (css: string, options?: { notActive?: stirng }) => CssString
Generates :hover
, :active
and :focus-visible
selectors in a backwards
compatible manner.
import {
css,
hoverKeyboardFocusAndActiveStyling,
} from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent__cardlink {
/* Card link styles ... */
${hoverKeyboardFocusAndActiveStyling(css`
border: 2px solid currentColor;
`)};
}
`;
By passing a second options
parameter, the :active
selector can be
skipped.
Sometimes you need to discourage certain HTML markup patterns, and Hanna provides some helpful mixin methods to that purpose.
They render a "hi-vis" outline around the current element and display a
message in either ::before
or ::after
content.
By default, these messages are only visible in CSS rendered when
isDevMode === true
.
Example usage:
import { WARNING__, WARNING_message__ } from '@reykjavik/hanna-css';
export default css`
:not(ul):not(ol) > li {
${WARNING__('<li/> must be inside <ol/> or <ul/>')};
}
ul > :not(li),
ol > :not(li) {
${WARNING__('<ul/> only accepts <li/> as its children')};
}
/* Override the message only (emits less code) */
ol > :not(li) {
${WARNING_message__('<ol/> only accepts <li/> as its children')};
}
`;
Syntax: WARNING__(message: string, opts?: WarningOpts): string
Renders a high-priority (red) warning and message.
Syntax: WARNING_soft__(message: string, opts?: WarningOpts): string
Renders a lower-priority (orange) warning and message that are only visible
then the HTML element is :hover
ed.
Syntax:
WARNING_soft__(message: string, ops?: Omit<WarningOpts, 'pos'>): string
Only sets (overrides) the warning message on an element that already has a warning style applied.
Syntax: WARNING_border__(ops?: Omit<WarningOpts, 'pos'>): string
Sets a high-priority (red) warning border around an element.
Syntax: WARNING_border_soft__(ops?: Omit<WarningOpts, 'pos'>): string
Renders a lower-priority (orange) warning warning that is only visible then
the HTML element is :hover
ed.
Syntax: suppress_WARNING__(ops?: WarningOpts): string
Attempts to remove warning border and message.
Syntax: suppress_WARNING_soft__(ops?: WarningOpts): string
Attempts to remove lower-priority (:hover
) warning border and message.
The WARNING_*
mixins accept an options object as their second argument.
WarningOpts.pos?: 'before' | 'after'
Default: 'before'
Controls into which ::pseudo-element the message
content is rendered.
WarningOpts.always?: boolean
Default: false
Optionally make the warning messages visible in production builds also. A drastic measure reserved for highly unusual situations.
Sometimes CSS lengths/sizes should scale lineraly with their viewport or
container width. For this Hanna pprovides series of scale*
and clamp*
helper functions.
The clamp_
methods return a clamp(A, calc(…), B)
value, while the
lower-level scale*
methods return a bare calc(…)
value.
All of these helpers accept from
and to
of
type ScaleEdge = PxValue | PctValue | number
as their first two parameters.
Syntax:
scale(from: ScaleEdge, to: ScaleEdge, min: number | PxValue, max: number | PxValue, unit: '%' | 'vw' | 'vh'): string
This generic, low-level scale
function lies at the heart of all of the other
scale*
and clamp*
helpers.
It returns a CSS calc(…)
function with slope+intercept values, that scales
from from
at a container/viewport size of min
, up to to
at a
container/viewport size of max
.
If the unit parameter is set to either vw
/vh
, then the min
and max
values refer to the viewport size.
import { css, px, pct } from `@reykjavik/hanna-css`
import { scale } from `@reykjavik/hanna-css/scale`
const myCSS = css`
div {
/* Supports "%" */
height: ${scale(16, 24, 320, 1368, '%')};
/* Supports "vw" (and "vh") */
width: ${scale(16, 24, 320, 1368, 'vw')};
/* Returns bare intercept when slopeFactor is 0 */
margin-top: ${scale(16, 16, 320, 1368, '%')};
/* Returns bare slope when intercept is 0 */
margin-bottom: ${scale(16, 64, 320, 1280, 'vh')};
}
`
/*
div {
/* Supports "%" */
height: calc(0.7633587786259541% + 13.557251908396948px);
/* Supports "vw" (and "vh") */
width: calc(0.7633587786259541vw + 13.557251908396948px);
/* Returns bare intercept when slopeFactor is zero */
margin-top: 16px;
/* Returns bare slope when intercept is zero */
margin-bottom: 5vh;
}
*/
These generate vw
-based responsive sizes, that scale linearly between two
end-point sizes (number
, PxValue
or PctValue
), within certain named
media-query boundries.
clamp_phone
, clamp_phablet
, clamp_tablet
, clamp_netbook
,
clamp_phone_netbook
, clamp_phablet_netbook
, clamp_tablet_netbook
,
clamp_phone_tablet
, clamp_phablet_tablet
, clamp_phone_phablet
.
scale_phone
, scale_phablet
, scale_tablet
, scale_netbook
,
scale_phone_netbook
, scale_phablet_netbook
, scale_tablet_netbook
,
scale_phone_tablet
, scale_phablet_tablet
, scale_phone_phablet
.
Example:
import { css } from '@reykjavik/hanna-css';
import { clamp_tablet_netbook } from '@reykjavik/hanna-css/scale';
const myCSS = css`
.CustomHeader {
height: ${clamp_tablet_netbook(60, 120)};
}
`;
/*
.CustomHeader {
height: clamp(60px, calc(9.868421052631579vw + -15px), 120px)
}
*/
Which is effectively the same as this using the corresponding scale_*
function:
import { css, mq, px } from '@reykjavik/hanna-css';
import { scale_tablet_netbook } from '@reykjavik/hanna-css/scale';
const from = px(60);
const to = px(120);
const myCSS = css`
@media screen {
.CustomHeader {
height: ${from};
}
}
@media screen and ${mq.tablet_netbook} {
.CustomHeader {
height: ${scale_tablet_netbook(from, to)};
}
}
@media screen and ${mq.wide} {
.CustomHeader {
height: ${to};
}
}
`;
Syntax: scale_container(from: ScaleEdge, to: ScaleEdge): string
This %
-based scaler works for elements directly within a full-grid wide
container. (As defined by Hanna's grid_raw.contentMinWidth
and
grid_raw.contentMaxWidth
).
Syntax:
scale_cols(from: ScaleEdge, to: ScaleEdge, cols: number, gutters?: number): string
Generates a %
-based calc()
value that scales linearly between from
and
to
inside a container whos width is certain nubmer of grid columns and
gutters.
Syntax: gridPx(columns: number, gutters?: number): PxValue
Returns a fixed pixel width value for grid layout styling. Mainly usable for
max-width
/min-width
boundaries.
NOTE: Make sure to use the scalable hannaVars.grid_*
variable tokens,
whenever possible.
import { css, gridPx, hannaVars } from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent {
display: flex;
flex-flow: row nowrap;
column-gap: ${hannaVars.grid_gutter};
}
.MyComponent__sideCol {
width: ${hannaVars.grid_3};
}
.MyComponent__mainCol {
width: ${hannaVars.grid_9};
max-width: ${gridPx(6)};
}
`;
Using the Hanna CSS variables is highly preferable, whenever possible. However, there are always edge cases where you need access to the raw values the CSS variables build on.
For that this library exports some helpful objects.
import {
breakpoints_raw,
colors_raw,
font_raw,
grid_raw,
iconfont_raw,
} from '@reykjavik/hanna-css';
Again use these sparingly, and deliberately.
Here are a few code "snippets" you can add to your global snippets file to help you use hanna-css a bit faster:
See CHANGELOG.md