Misc. JS/TS helpers used by Reykjavík City's web dev teams.
npm install @reykjavik/webtoolsMiscellaneous JavaScript/TypeScript helpers used by Reykjavík City's web dev
teams.
This library is split up into multiple individual modules to help keep your
bundles slim and aid tree-shaking.
```
npm install @reykjavik/webtools
yarn add @reykjavik/webtools
bun add @reykjavik/webtools
Contents:
- @reykjavik/webtools/http
- HTTP Status Codes
- Types for HTTP Status code groups
- cacheControl helper
- cacheControlHeaders helper
- Type TTLConfig
- toSec TTL helper
- toMs duration helper
- @reykjavik/webtools/fixIcelandicLocale
- Limitations
- @reykjavik/webtools/async
- promiseAllObject
- maxWait
- debounce
- throttle
- @reykjavik/webtools/hoooks
- useDebounced
- useThrottled
- @reykjavik/webtools/errorhandling
- asError
- Result Singleton
- Type ResultTuple
- Type ResultTupleObj
- Type ResultTupleObj.mapTo
- Result.catch
- Result.ify
- Result.map
- Result.Success
- Result.Fail
- Result.throw
- Type Result.PayloadOf
- @reykjavik/webtools/SiteImprove
- SiteImprove component
- pingSiteImprove helper
- pingSiteImproveOutbound helper
- @reykjavik/webtools/CookieHubConsent
- CookieHubProvider component
- useCookieHubConsent
- @reykjavik/webtools/vanillaExtract
- vanillaClass
- vanillaGlobal
- vanillaProps
- vanillaVars
- Framework Specific Tools
- React-Router Tools
- Next.js Tools
- Contributing
- Changelog
---
Various framework agnostic helpers for leveraging HTTP magic.
All the web-related HTTP status codes are exported with human-readable names
and a short JSDoc comment:
- HTTP_200_OKHTTP_303_SeeOther
- HTTP_304_NotModified
- HTTP_307_TemporaryRedirect
- HTTP_308_PermanentRedirect
- HTTP_400_BadRequest
- HTTP_401_Unauthorized
- HTTP_403_Forbidden
- HTTP_404_NotFound
- HTTP_418_ImATeapot
- HTTP_500_InternalServerError
-
- ...ad nauseum.
These make your code more readable and less prone to accidental mistakes:
`ts
import { HTTP_200_OK, HTTP_404_NotFound } from '@reykjavik/webtools/http';
console.log(HTTP_200_OK); // 200
console.log(HTTP_404_NotFound); // 404
`
These type unions are useful when writing HTTP helper functions and error
handlers, etc.
Union Types for the more commonly occurrring HTTP Status codes:
- HTTP_INFO (100, 101)HTTP_SUCCESS
- (200, 201, 202)HTTP_REDIRECTION
- (301, 302, 303, 304, 307, 308)HTTP_NOTMODIFIED
- (304)HTTP_ERROR
- (400, 404, 410, 401, 403, 500)HTTP_CLIENT_ERROR
- (400, 404, 410, 401, 403)HTTP_NOT_FOUND
- (400, 404, 410)HTTP_BANNED
- (401, 403)HTTP_SERVER_ERROR
- (500)
It also offers more complete union types, including all the esoteric status
codes, are also available:
- HTTP_STATUS (all the status-codes!)HTTP_INFO_ALL
- (1\\)HTTP_SUCCESS_ALL
- (2\\)HTTP_REDIRECTION_ALL
- (3\\)HTTP_ERROR_ALL
- (4\\ and 5\\)HTTP_CLIENT_ERROR_ALL
- (4\\)HTTP_SERVER_ERROR_ALL
- (5\\)
Syntax:
cacheConrol(response: ServerResponse | Response | Map
Use this function to quickly set the Cache-Control header with a max-age=Map
on a HTTP response (or a object representing response headers).
`js
import { cacheControl } from '@reykjavik/webtools/http';
// ...then inside an API handler
// or a framework's data loader function
cacheControl(res, '4h');
// ...then set statusCode and send data
`
The directives private and immutable are used by by default.
Use the optional eTag parameter if you intend to
handle conditional requests.
Syntax:
cacheControlHeaders(ttlCfg: TTLConfig, eTag?: string|number): Record
Similar to the cacheControl helper, but returns an
plain object with the headers for use in situations where HeadersInit object
are expected.
`js
import { cacheControlHeaders } from '@reykjavik/webtools/http';
const response = new Response('Hello, World!', {
headers: cacheControlHeaders('4h'),
});
`
`js
`
#### Type TTLConfig
`js
import type { TTLConfig } from '@reykjavik/webtools/http';
const myTTL1: TTLConfig = '4s';
const myTTL2: TTLConfig = { maxAge: '4s' };
`
The ttlCfg parameter is either a bare TTL (max-age) value:
- number — seconds"${number}${'s'|'m'|'h'|'d'|'w'}"
- — gets converted to seconds
…one of these TTLKeywords:
- "permanent" — an alias for maxAge: '365d'"no-cache"
- — disables caching"unset"
- — removes the header altogether
…or a more complex TTLConfig object with the following properties:
TTLConfig.maxAge: TTL | TTLKeywords (required)
Sets the max-age= directive. See above definitions
(NOTE: Second values of zero or less get converted to "no-cache".)
TTLConfig.staleWhileRevalidate?: TTL
If set to a positive value then stale-while-revalidate= is added to the
response header
TTLConfig.staleIfError?: TTL
If set to a positive value then stale-if-error= is added to the response
header
TTLConfig.publ?: boolean
Sets the response caching as "public", instead of the default "private"
TTLConfig.stability?: 'revalidate' | 'immutable' | 'normal'
Allows setting a "must-revalidate" flag instead of the default "immutable". A
value of "normal" omits the flagging and falls back to HTTP's default
behavior.
Syntax: toSec(ttl: number | ${number}${'s'|'m'|'h'|'d'|'w'}): number
Converts a TTL (max-age) value into seconds. Returns 0 for bad and/or
negative input values.
`js
import type { toSec, TTL } from '@reykjavik/webtools/http';
const ttl: TTL = '2h';
const ttlSec1 = toSec(ttl); // 7200
// Raw numbers are returned as-is (rounded)
const ttlSec2 = toSec(10.6); // 11
// Negative numbers become zero
const ttlSec3 = toSec('-1h'); // 0
`
Syntax: toSec(duration: number | ${number}${'s'|'m'|'h'|'d'|'w'}): number
Converts a TTL (duration) value into milliseconds. Returns 0 for bad
and/or negative input values.
`js
import type { toMs, TTL } from '@reykjavik/webtools/http';
const ttl: TTL = '2h';
const ttlMs1 = toMs(ttl); // 7_200_000
// Raw numbers are returned as-is (rounded)
const ttlMs2 = toMs(499.9); // 500
// Negative numbers become zero
const ttlMs3 = toMs('-1h'); // 0
`
---
As of early 2024, Google Chrome still does not support the Icelandic locale
is/is-IS in any way. Meanwhile other browsers have supported it for over a
decade.
This module patches the following methods/classes by substituting the isda
locale with (Danish) and apply a few post-hoc fixes to their return
values.
- Intl.Collator and String.prototype.localeCompare (\*)Intl.NumberFormat
- and Number.prototype.toLocaleString (\*)Intl.DateTimeFormat
- and Date.prototype.toLocaleString,.toLocaleDateString
, and .toLocaleTimeString (\*)Intl.RelativeDateFormat
- Intl.PluralRules
- Intl.ListFormat
-
(\*) The results are quite usable, but not entirely perfect. The
limitations/caveats are listed below.
To apply the patch, simply "side-effect import" this module at the top of your
app's entry point:
`ts
import '@reykjavik/webtools/fixIcelandicLocale';
// Then continue with your day and use localeCompare and other Intl.* methods`
// as you normally would. (See "limitations" below.)
(NOTE The patch is only applied in engines that fail a simple feature
detection test.)
Intl.Collator and localeCompare:
- It sorts initial letters correctly but in the rest of the string, it
incorrectly treats ð and d as the same letter (most of the time), andá
lumps the acute-accented characters , é, í, ó, ú and ý in with
their non-accented counterparts.
Intl.NumberFormat and toLocaleString:
- The style: "unit" option is not supported and prints units in Danish. (SoocurrencyDisplay: "name"
many units and unit-variants…)
- The option is not supported and prints the
currency's full name in Danish.
Intl.DateTimeFormat and toLocaleDateString:
- The month: 'narrow' and weekday: 'narrow' options are not supported, andtimeZoneName
print the corresponding Danish initials.
- For the values "long", "shortGeneric" and "longGeneric"timeStyle: 'full'
will appear in Danish.
- The option prints the timezone names in DanishdayPeriod
- The option has a couple of slight mismatches, at 5 am and 12
noon.
We eagerly accept bugfixes, additions, etc. to this module!
---
Contains a few small helpers for working with async functions and promises.
---
Syntax:
promiseAllObject
A variation of Promise.all() that accepts an object with named promises and
returns a same-shaped object with the resolved values.
`ts
import { promiseAllObject } from '@reykjavik/webtools/async';
const { user, posts } = await promiseAllObject({
user: fetchUser(),
posts: fetchPosts(),
});
`
---
Syntax: maxWait(timeout: number, promises: Array maxWait
Syntax:
This somewhat esoteric helper resolves soon when all of the passed promisestimeout
have settled (resolved or rejected), OR after milliseconds —
whichever comes first.
If an object is passed, the resolved value will be an object with the same
keys, and any settled values in a PromiseSettledResult object, andundefined for any promises that didn't settle in time.
`ts
import { maxWait } from '@reykjavik/webtools/async';
const user = fetchUser(); // Promise
const posts = fetchPosts(); // Promise
// Array of promises resolves to void
await maxWait(500, [user, posts]);
// Object of promises resolves to an object with any resolved values at that time
const { user, posts } = await maxWait(500, { user, posts });
console.log(user?.value); // undefined | User
console.log(posts?.value); // undefined | Array
console.log(posts?.status); // 'fulfilled' | 'rejected'
console.log(posts?.reason); // undefined | unknown
`
---
Syntax:
debounce>(func: (...args: A) => void, delay: number, immediate?: boolean): ((...args: A) => void) & { cancel: (finish?: boolean) => void; }
Returns a debounced function that only runs after delay milliseconds ofimmediate
quiet-time, and can optionally be made to run ly on first call
before dbouncing subsequent calls.
`ts
import { debounce } from '@reykjavik/webtools/async';
// Basic usage:
const sayHello = debounce((namme: string) => {
console.log('Hello ' + name);
}, 200);
sayHello('Alice');
sayHello('Bob');
sayHello('Charlie');
sayHello('Dorothy');
// Only "Hello Dorothy" is logged, 200ms after the last call
// With immediate param set to true:`
const sayHi = debounce(
(namme: string) => console.log('Hi ' + name),
200,
true
);
sayHi('Alice');
sayHi('Bob');
sayHi('Charlie');
sayHi('Dorothy');
// "Hi Alice" is logged immediately
// Then "Hi Dorothy" is logged, 200ms after the last call
The returned function has a nice .cancel() method, which can optionally
invoke the function before cancelling, if it had a debounce pending.
`ts
sayHello('Erica');
sayHello('Fiona');
sayHello.cancel();
// Nothing is logged because the debounce was cancelled
sayHello('George');
sayHello('Harold');
sayHello.cancel(true); // finish parmeter is true`
// "Hello Harold" is logged immediately because it was pending
---
Syntax:
throttle>(func: (...args: A) => void, delay: number, skipFirst?: boolean): ((...args: A) => void) & { finish: (cancel?: boolean) => void; }
Returns a throttled function that never runs more often than every delayskipFirst
milliseconds. It can optionally made to invocation.
`ts
import { throttle } from '@reykjavik/webtools/async';
// Basic usage:
const sayHello = throttle((name: string) => {
console.log('Hello ' + name);
}, 200);
sayHello('Alice');
sayHello('Bob');
sayHello('Charlie');
sayHello('Dorothy');
// Only "Hello Alice" is logged immediately. The other calls were throttled.
// With skipFirst param set to true:`
const sayHi = throttle(
(name: string) => console.log('Hi ' + name),
200,
true
);
sayHi('Alice');
sayHi('Bob');
sayHi('Charlie');
sayHi('Dorothy');
// Nothing is logged. The first call was skipped, and the rest were throttled.
The returned function also has a nice .finish() method to reset the throttle
timer. By default it instantly invokes the function, if the last call was
throttled (skipped)-.
`ts
sayHello('Erica');
sayHello('Fiona');
sayHello.finish();
// "Hello Fiona" is logged immediately because it was pending
sayHello('George');
sayHello('Harold');
sayHello.finish(true); // cancel parmeter is true`
// Nothing is logged because the pending call was cancelled
---
Some useful React hooks.
Syntax:
useDebounced>(func: (...args: A) => void, delay: number, immediate?: boolean): ((...args: A) => void) & { cancel: (finish?: boolean) => void; }
Returns a stable debounced function that invokes the supplied function after
the specified delay. When the component unmounts, any pending (debounced)
calls are automatically cancelled.
NOTE: The supplied callback does not need to be memoized. The debouncer
will always invoke the last supplied version.
`ts
import { useDebounced } from '@reykjavik/webtools/hoooks';
const MyComponent = () => {
const renderDate = new Date();
const debouncedSearch = useDebounced((query: string) => {
console.log('Searching for:', query, 'at', renderDate.toISOString());
}, 200);
return (
type="text"
onChange={(e) => {
debouncedSearch(e.currentTarget.value);
}}
/>
);
};
`
See debounce for more details about the parameters and the
returned debounced function's .cancel() method.
Syntax:
useThrottled>(func: (...args: A) => void, delay: number, skipFirst?: boolean): ((...args: A) => void) & { finish: (cancel?: boolean) => void; }
Returns a stable throttler function that throttles the supplied function.
NOTE: The supplied callback does not need to be memoized. The throttler
will always invoke the last supplied version.
`ts
import { useThrottled } from '@reykjavik/webtools/hoooks';
const MyComponent = () => {
const renderDate = new Date();
const throttledReportPosition = useThrottled((x: number, y: number) => {
console.log('Mouse position:', x, ',', y, 'at', renderDate.toISOString());
}, 200);
return (
throttle for more details about the parameters and the
returned throttled function's .finish() method.---
@reykjavik/webtools/errorhandlingA small set of lightweight tools for handling errors and promises in a safer,
more structured, FP-ish way.
Errors are always the first return value to promote early, explicit error
handling.
$3
Syntax:
asError(maybeError: unknown): ErrorFromPayloadGuarantees that a caught (
catch (e)) value of unknown type, is indeed an
Error instance.If the input is an
Error instance, it is returned as-is. If the input is
something else it is wrapped in a new ErrorFromPayload instance, and the
original value is stored in as a payload property, and it's .toString() is
used for the message property.`ts
import { asError, type ErrorFromPayload } from '@reykjavik/webtools/errorhandling';const theError = new Error('Something went wrong');
try {
throw theError;
} catch (err) {
// theError is an instance of Error so it's returned as-is
const error = asError(theError);
console.error(error === theError); // true
console.error('payload' in error); // false
}
const someObject = ['Oops', 'Something went wrong'];
try {
throw someObject;
} catch (err) {
// the thrown someObject is not an Error so an
ErrorFromPayload is returned
const error = asError(someObject);
console.error(error === someObject); // false
console.error(error instanceOf ErrorFromPayload); // true console.error(error.payload === someObject); // true
console.error(error.message === someObject.join(',')); // true
}
`$3
Singleton object with the following small methods for creating, mapping or
handling
ResultTupleObj instances:-
Result.Success
- Result.Fail
- Result.catch
- Result.map
- Result.throw$3
Syntax:
ResultTuple(Also aliased as
Result.Tuple)Simple bare-bones discriminated tuple type for a
[error, result] pair.`ts
import { type ResultTuple } from '@reykjavik/webtools/errorhandling';declare const myResult: ResultTuple;
const [error, result] = myResult;
// (One of these two is always
undefined)if (error) {
// Here
error is an Error instance
console.error(error.message);
} else {
// Here result is guaranteed to be a string
console.log(result);
}
`$3
Syntax:
ResultTupleObj(Also aliased as
Result.TupleObj)Discriminated tuple type for a
[error, result] pair (same as ResultTuple)
but with named properties error and result attached for dev convenience.It also has a
.mapTo method (see below).`ts
import { type ResultTupleObj } from '@reykjavik/webtools/errorhandling';declare const myResult: ResultTupleObj;
const [error, result] = myResult;
// (One of these two is always
undefined)if (error) {
// Here
error is an Error instance
console.error(error.message);
} else {
// Here result is guaranteed to be a string
console.log(result);
}// But
myResults also has named properties, for convenience
if (myResult.error) {
// Here myResult.error is an Error instance
console.error(myResult.error.message);
} else {
// Here myResult.result is a string
console.log(myResult.result);
}
`#### Type
ResultTupleObj.mapToSyntax:
ResultTupleObj.mapToThis convenience method allows quick mapping of the
ResultTubleOBj's result
value to a new type. The returned value is also a ResultTubleOBj.Result.map.)`ts
import { type ResultTuple } from '@reykjavik/webtools/errorhandling';declare const myResult: ResultTuple;
const mappedResult: ResultTupleObj = myResult.mapTo(
(result: string) => result.length
);
if (mappedRes.error) {
console.error(myResult.error.message);
} else {
// Here
myResult.result is a number
console.log(myResult.result);
}
`If the original
ResultTupleObj is in a failed state, the mapping function is
not called.If the mapping function throws an error it gets caught and turned into a
failed
ResultTupleObj.$3
Aliased as
Result.ify for readability.Syntax:
Result.catch
Syntax:
Result.catchError handling utility that wraps a promise or a callback function.
Catches errors and returns a
ResultTupleObj — a nice discriminated
[error, results] tuple with the result and error also attached as named
properties.Works on both promises and sync callback functions.
`ts
import { Result } from '@reykjavik/webtools/errorhandling';// Callback:
const [error, fooObject] = Result.catch(() => getFooSyncMayThrow());
// Promise:
const [error, fooObject] = await Result.catch(getFooPromiseMayThrow());
// Example of object property access:
const fooQuery = await Result.catch(getFooPromiseMayThrow());
if (fooQuery.error) {
console.log(fooQuery.error === fooQuery[0]); // true
throw fooQuery.error;
}
console.log(fooQuery.result === fooQuery[1]); // true
fooQuery.result; // Guaranteed to be defined
`Result.throw().$3
Result.catch.$3
Syntax:
Result.mapConvenience helper to map a
ResultTuple-like object to a new
ResultTupleObj object, applying a transformation function to the result, but
retaining the error as-is. Errors thrown from the mapping function are caught
and turned into a failed ResultTupleObj.`ts
import { Result } from '@reykjavik/webtools/errorhandling';const getStrLength = (str: string) => str.length;
const resultTuple =
Math.random() < 0.5 ? [new Error('Fail')] : [undefined, 'Hello!'];
const [error, mappedResult] = Result.map(resultTuple, getStrLength);
if (result) {
console.log(result); // 6
}
`$3
Syntax:
Result.SuccessFactory for creating a successful
ResultTupleObj.`ts
import { Result } from '@reykjavik/webtools/errorhandling';const happyResult: Result.SuccessObj =
Result.Success('My result value');
console.log(happyResult.error); // undefined
console.log(happyResult[0]); // undefined
console.log(happyResult.result); // 'My result value'
console.log(happyResult[1]); // 'My result value'
`$3
Syntax:
Result.FailFactory for creating a failed
ResultTupleObj.`ts
import { Result } from '@reykjavik/webtools/errorhandling';const happyResult: Result.FailObj = Result.Fail(new Error('Oh no!'));
console.log(happyResult.error.message); // 'Oh no!'
console.log(happyResult[0].message); // 'Oh no!'
console.log(happyResult.result); // undefined
console.log(happyResult[1]); // undefined
`$3
Syntax:
Result.throwUnwraps a discriminated
ResultTuple-like [error, result] tuple and throws
if there's an error, but returns the result otherwise.`ts
import { Result } from '@reykjavik/webtools/errorhandling';try {
const fooResults = Result.throw(await getFooResultsTuple());
} catch (fooError) {
// Do something with the error from
getFooResultsTuple()
}
`Result.catch().$3
Syntax:
Result.PayloadOfThis utility type extracts the successful payload type
T from a
Result.Tuple-like type, a Promise of such type, or a function returning
either of those.`ts
import { Result } from '@reykjavik/webtools/errorhandling';type ResTpl = Result.Tuple;
type ResTplPromise = Promise>;
type ResTplFn = (arg: unknown) => Result.Tuple;
type ResTplPromiseFn = (arg: unknown) => Promise>;
type Payload1 = Result.PayloadOf; // string
type Payload2 = Result.PayloadOf; // number
type Payload3 = Result.PayloadOf; // boolean
type Payload4 = Result.PayloadOf; // Date
`---
@reykjavik/webtools/SiteImproveContains React helpers for loading SiteImprove's analytics scripts, and
perform page-view and custom event tracking in applications with client-side
(
pushState) routing.$3
A component for loading a SiteImprove analytics script and set up page-view
tracking across URL routes.
It also automatically logs all out-bound link clicks, to match the behavior of
the vanilla SiteImprove script.
Props:
The Component's props have detailed JSDoc comments (displayed in your code
editor), but there's a brief summary:
-
accountId?: string — Your SiteImprove account ID. (alternative to
scriptUrl prop).
- scriptUrl?: string — The full SiteImprove analytics script URL.
(alternative to accountId prop).
- hasConsented?: boolean — Manual GDPR 'analytics' consent flag. A false
value allows hard opt-out, but defers to
CookieHubProvider values if they are available.
Defaults to undefined which means "ask CookieHub if available, otherwise
no".
- onLoad?: (e: unknown) => void — Fires when the script has loaded.
- onError?: (e: unknown) => void — Fires if loading the script failed.Example usage somewhere in your application:
`jsz
import { SiteImprove } from '@reykjavik/webtools/SiteImprove';const siteImproveAccountId = '[ACCOUNT_ID]'; // e.g. "7654321"
// ...then inside your main App component
;
`In dev mode it does NOT load the SiteImprove script and merely logs page-view
events to the console.
$3
Syntax:
pingSiteImprove(category: string, action: string, label?: string): voidA small helper for tracking custom UI events and reporting them to SiteImrove.
It safely manages GDPR consent, so you can use it unconditionally.
`js
import { pingSiteImprove } from '@reykjavik/webtools/SiteImprove';const handleSubmit = () => {
// perform submit action...
if (success) {
pingSiteImprove('application', 'add_new');
}
};
`$3
Syntax:
pingSiteImproveOutbound(ourl: string): voidA small helper for reporting to SiteImrove when the user is programmatically
being sent to a different URL/resource.
`js
import { pingSiteImproveOutbound } from '@reykjavik/webtools/SiteImprove';const handleSubmit = () => {
// perform submit action...
if (success) {
const fileUrl = '/download/report.pdf';
pingSiteImproveOutbound(fileUrl);
document.location.href = fileUrl;
}
};
`---
@reykjavik/webtools/CookieHubConsentContains React helpers for loading CookieHub's consent manager and reading
users' consent values.
$3
This context provider component loads and initialises the CookieHub consent
management script and sets up a React state object with the relevant user
consent flags.
Wrap this provider around your component tree, and then use the
useCookieHubConsent() hook to retrieve live consent
information, whereever you wish to set GDPR-affected cookies or perform any
sort of tracking/logging.`js
import { CookieHubProvider } from '@reykjavik/webtools/CookieHubConsent';import { AnalyticsStuff } from '../components/AnalyticsStuff';
// Maybe move this to an Env variable, or something...
const cookiehubAccountId = '[ACCOUNT_ID]'; // e.g. "a4b3c2d1"
export default function App() {
return (
...my App UI...
);
}
`Props:
The Component's props have detailed JSDoc comments (displayed in your code
editor), but there's a brief summary:
-
accountId?: string | undefined — Your CookieHub account ID. (alternative
to scriptUrl prop). Pass undefined to skip loading the script.
- scriptUrl?: string — The full CookieHub embed script URL. (alternative to
accountId prop).
- options?: CookieHubOptions — Raw CookieHub options object that gets used
when the script initializes.
- onError?: OnErrorEventHandlerNonNull — Fires if loading the script failed.$3
Syntax:
useCookieHubConsent(): RecordReturns up-to-date cookie consent
boolean flags. For use in React components
or hook functions.`js
import { useCookieHubConsent } from '@reykjavik/webtools/CookieHubConsent';export const AnalyticsStuff = (props) => {
const consent = useCookieHubConsent();
if (!consent.analytics) {
return null;
}
// Perform analytics...
};
`If the
CookieHubProvider is missing from the VDOM tree above your component,
this hook will return an empty object.---
@reykjavik/webtools/vanillaExtractContains helpers for writing vanilla-extract
styles using plain CSS styntax.
This provides an "escape hatch" into regular CSS, when you're willing to trade
local type-safety for access to the full features and expressiveness of real
CSS.
(Background info.)
$3
Syntax:
vanillaClass(css: string | ((className: string, classNameSelector: string) => string)): string
Syntax:
vanillaClass(debugId: string, css: string | ((className: string, classNameSelector: string) => string)): stringReturns a scoped cssClassName styled with free-form CSS. This function is a
thin wrapper around vanilla-extract's
style function.When you pass it a string, all
&& tokens are automatically replaced with the
selector for the auto-generated class-name. Note that in such cases EVERY
style property must be wrapped in a selector block.To opt out of the
&& replacement, use the callback function signature.`ts
// someFile.css.ts
import { vanillaClass } from '@reykjavik/webtools/vanillaExtract';// 1) Simple class selector block — no sub-selectors — auto-wrapped
// in a class-name selector block.
export const myClass = vanillaClass(
);
// Generated CSS:
/*
.x1y2z3 {
background-color: #ccc;
padding: .5em 1em;
}
*/// ---------------------------------------------------------------------------
// 2) More advanced usage with
&& tokens that get replaced with the
// generated class-name selector (prefixed with a dot).
export const myClasWithAmp = vanillaClass(
&&
tokens are otherwise present */
);
// Generated CSS:
/*
.y2X1z3 {
background-color: #ccc;
padding: .5em 1em;
}
.y2X1z3 > strong {
color: #c00;
}
@media (min-width: 800px) {
.y2X1z3 {
background-color: #eee;
}
}
/ NOTE: Root-level CSS rules are NOT auto-wrapped when && tokens are otherwise present /
color: blue;
*/// ---------------------------------------------------------------------------
// 3) Advanced use: Pass a function to get the raw generated class-name,
// plus a more convenient dot-prefixed selector for the class-name.
export const myOtherClass = vanillaClass(
(classNameRaw, classNameSelector) =>
);
// Generated CSS:
/*
.y3z1X2 {
background-color: #ccc;
padding: .5em 1em;
}
[class="y3z1X2"] > strong {
color: #c00;
}
@media (min-width: 800px) {
.y3z1X2 {
background-color: #eee;
}
}
/ NOTE: '&&' tokens returned from a callback function are NOT replaced /
&& { this-is-not: interpolated; }
*/// ---------------------------------------------------------------------------
// 4) ...with a human readable debugId
export const humanReadableClass = vanillaClass(
'HumanReadable__classNamePrefix',
);
// Generated CSS:
/*
.HumanReadable__classNamePrefix_x2y1z3 {
border: 1px dashed hotpink;
cursor: pointer;
}
*/
`(NOTE: The dot-prefixed
&& pattern was chosen as to not conflict with the
bare & token in modern nested CSS.)$3
Syntax:
vanillaGlobal(css: string): voidInserts free-form CSS as a vanilla-extract
globalStyle.`ts
// someFile.css.ts
import { vanillaGlobal } from '@reykjavik/webtools/vanillaExtract';vanillaGlobal(
);
`$3
Syntax:
vanillaProps(css: string): GlobalStyleRuleReturns an object that can be safely spread into a vanilla-extract style
object, to inject free-form CSS properties (or nested blocks).
`ts
// someFile.css.ts
import { style } from '@vanilla-extract/css';
import { vanillaProps } from '@reykjavik/webtools/vanillaExtract';const myStyle = style({
color: 'darksalmon',
// ...other style props...
...vanillaProps(
),
});
`$3
Syntax:
vanillaVars(...varNames: Array): Record < var${Capitalize, string> Returns an object with privately scoped CSS variables props. Pass them around
and use them in your CSS.
`ts
// MyComponent.css.ts
import {
vanillaVars,
vanillaGlobal,
} from '@reykjavik/webtools/vanillaExtract';const { varPrimaryColor, varSecondaryColor, setVars } = vanillaVars(
'primaryColor',
'secondaryColor'
);
export { varPrimaryColor, varSecondaryColor };
export const wrapper = vanillaClass(
);
`…and then in your component:
`ts
// MyComponent.tsx
import React from 'react';
import * as cl from './someFile.css.ts';export function MyComponent() {
return (
className={cl.wrapper}
style={{
[cl.varPrimaryColor]: 'yellow',
[cl.varSecondaryColor]: 'blue',
}}
>
...children...
---
Framework Specific Tools
$3
See README-rr.md for helpers and components specifically
designed for use in React-router projects.
(NOTE: If you're still using Remix.run you can install
version
"^0.1.22"` of this package.)See README-nextjs.md for helpers and components
specifically designed for use in Next.js projects.
---
This project uses the Bun runtime for development (tests,
build, etc.)
PRs are welcome!
---
See
CHANGELOG.md