@reykjavik/hanna-css
v0.4.15
Published
TypeScript helpers for developers writing custom CSS additions for Hanna-based designs.
Downloads
295
Readme
@reykjavik/hanna-css
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
Why TypeScript Instead of SASS?
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. ❤️
Generic CSS helpers
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".
Hanna CSS Variables
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.
hannaVars
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);
}
`*/
hannaVarOverride
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;
}
`*/
formFieldVars
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...
}
`*/
buildVariables
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);
}
`*/
Class-Name constants
htmlCl
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.
Helpful Constants
colorThemes
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"
colorFamilies
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"
characters
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æ!“"
icons
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"
mq
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%;
}
}
`*/
Hanna CSS Env
getCssBundleUrl
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.
Type CssBundleOpts
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 ———^^^^ */
Type CssModuleToken
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.
getEssentialHannaScripts
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.
styleServerUrl
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()
setStyleServerUrl
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.
targetCssVersion
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.
isDevMode
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'};
}
`;
Accessibility Helpers
srOnly
mixin
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.)
srOnly_focusable
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};
}
`;
srOnly_focusableContent
mixin
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};
}
`;
keyboardFocusStyling
mixin
Syntax: keyboardFocusStyling: (css: string) => CssString
Generates backwards compatible selectors for :focus-visible
— and also
targets the class-names injected by the
focus-visible
polyfill
import { css, keyboardFocusStyling } from '@reykjavik/hanna-css';
const myCss = css`
.MyComponent__cardlink {
/* Card link styles ... */
/* :hover styles ... */
${keyboardFocusStyling(css`
outline: 2px solid currentColor;
outline-offset: 2px;
`)};
}
`;
hoverKeyboardFocusAndActiveStyling
mixin
Syntax:
hoverKeyboardFocusAndActiveStyling: (css: string, options?: { notActive?: stirng }) => CssString
Generates :hover
, :active
and :focus-visible
selectors in a backwards
compatible manner. (It also targets the class-names injected by the
focus-visible
polyfill)
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.
Markup Warning Helpers
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')};
}
`;
WARNING__
Syntax: WARNING__(message: string, opts?: WarningOpts): string
Renders a high-priority (red) warning and message.
WARNING_soft__
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.
WARNING_message__
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.
WARNING_border__
Syntax: WARNING_border__(ops?: Omit<WarningOpts, 'pos'>): string
Sets a high-priority (red) warning border around an element.
WARNING_border_soft__
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.
suppress_WARNING__
Syntax: suppress_WARNING__(ops?: WarningOpts): string
Attempts to remove warning border and message.
suppress_WARNING_soft__
Syntax: suppress_WARNING_soft__(ops?: WarningOpts): string
Attempts to remove lower-priority (:hover
) warning border and message.
Type WarningOpts
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.
Scaling Helpers
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.
Generic Scaler
scale
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;
}
*/
Media Bracket Scalers
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};
}
}
`;
Container Relative Scalers
scale_container
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
).
scale_cols
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.
Raw Design Constants
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.
Helpful VSCode Snippets
Here are a few code "snippets" you can add to your global snippets file to help you use hanna-css a bit faster:
"Insert ${} variable print block": {
"scope": "javascript,javascriptreact,typescript,typescriptreact,css",
"prefix": "v",
"body": "\\${$0}",
},
"Insert Hanna CSS variable": {
"scope": "css",
"prefix": "h",
"body": "\\${hannaVars.$1};",
},
"Insert CSS property with Hanna varible": {
"scope": "css",
"prefix": "hannaprop",
"body": "${1:css-property}: \\${hannaVars.$2};",
},
"css`` tagged template literal": {
"scope": "javascript,javascriptreact,typescript,typescriptreact",
"prefix": "css",
"body": "css`\n\t$0\n`",
},
"cssVal`` tagged template literal": {
"scope": "javascript,javascriptreact,typescript,typescriptreact",
"prefix": "cssVal",
"body": "cssVal`\n\t$0\n`",
},
Changelog
See CHANGELOG.md