A sketch field component for React applications, built on fabric.js. Draw with a pencil, lines, arrows, rectangles and circles, select and move objects, add images and text, undo and redo, and export the drawing as JSON or as an image. The package includes TypeScript declarations.
The showcase is live at https://tbolis.github.io/react-sketch/.
yarn add react-sketchor
npm install react-sketchreact (^17.0.0 || ^18.0.0 || ^19.0.0) is a peer dependency: install it in your application.
fabric is a dependency of react-sketch and is installed with it.
SketchField is a class component. The methods of the component (undo, toJSON, ...) are available
through a ref, from the moment the component is mounted until it is unmounted.
import { useRef, useState } from "react";
import { SketchField, Tools } from "react-sketch";
export const Drawing = () => {
const sketch = useRef<SketchField>(null);
const [canUndo, setCanUndo] = useState(false);
return (
<>
<SketchField
ref={sketch}
width={1024}
height={768}
tool={Tools.Pencil}
lineColor="black"
lineWidth={3}
onChange={() => setCanUndo(sketch.current?.canUndo() ?? false)}
/>
<button disabled={!canUndo} onClick={() => sketch.current?.undo()}>
Undo
</button>
</>
);
};All types are available in the Types namespace, for example Types.SketchProperties,
Types.SketchEvent and the Types.Tool enumeration:
import { SketchField, Types } from "react-sketch";
const onObjectAdded = (event: Types.SketchEvent) => {
console.log("added a", event.target?.type);
};
export const Highlight = () => (
<SketchField
tool={Types.Tool.HIGHLIGHTER}
lineColor="#ffcc00"
onObjectAdded={onObjectAdded}
/>
);import { useRef } from "react";
import { SketchField, Tools } from "react-sketch";
export const Drawing = () => {
const sketch = useRef(null);
return (
<div>
<SketchField
ref={sketch}
width={1024}
height={768}
tool={Tools.Pencil}
lineColor="black"
lineWidth={3}
/>
<button onClick={() => sketch.current.clear()}>Clear</button>
</div>
);
};The default export of 0.5.x keeps working. It is the component, which also carries SketchField and
Tools:
import Sketch from "react-sketch";
export const Drawing = () => <Sketch tool={Sketch.Tools.Pencil} lineWidth={3} />;toJSON() returns the drawing as an object. defaultValue and value accept that object, or the same
object as a JSON string. The object has the format of fabric 7 (object types such as "Rect" and
"IText"). Drawings saved with react-sketch 0.5.x load as well, drawings saved with 1.0.0 do not load
in 0.5.x.
defaultValueis loaded when the component mounts, unlessvalueis set.valueis loaded when it changes (a different object, compared by identity). Pass a new object, for example{ ...saved }, to load the same drawing again, or setforceValueto loadvalueon every update of the component.- The drawing is loaded 100 ms later, and
onChangeis called when the load is done. The undo history starts again with the loaded objects, so each of them can be undone, andonObjectAddedis called for each. - A value which cannot be loaded (not valid JSON, or an object type which is not known) leaves the canvas and its undo history as they were, and the error is thrown as an uncaught error: it is not hidden. A newer value cancels the load of an older one which is still running, so the last value is the one shown.
valueis not updated while the user draws. Read the drawing withtoJSON(). Do not store the result oftoJSON()invaluefromonChange: loading a value callsonChange, so the value would be loaded again and again.- Images added with
addImgand the background image are part of the JSON, so onevalueordefaultValueholds shapes and images. Loading replaces everything, the background image included: add images afteronChangereports the load.
import { useRef, useState } from "react";
import { SketchField, Tools, Types } from "react-sketch";
export const SaveAndLoad = ({ initial }: { initial: Types.SketchValue }) => {
const sketch = useRef<SketchField>(null);
const [saved, setSaved] = useState<Types.SketchData>();
const [value, setValue] = useState<Types.SketchValue>();
return (
<>
<SketchField
ref={sketch}
tool={Tools.Pencil}
defaultValue={initial}
value={value}
/>
<button onClick={() => setSaved(sketch.current?.toJSON())}>Save</button>
<button disabled={!saved} onClick={() => setValue(saved && { ...saved })}>
Load
</button>
</>
);
};import { useRef } from "react";
import { SketchField, Tools } from "react-sketch";
export const Editor = ({ imageUrl }: { imageUrl: string }) => {
const sketch = useRef<SketchField>(null);
const download = () => {
const link = document.createElement("a");
link.href = sketch.current?.toDataURL({ format: "png", multiplier: 2 }) ?? "";
link.download = "sketch.png";
link.click();
};
return (
<>
<SketchField ref={sketch} tool={Tools.Select} backgroundColor="white" />
<button onClick={() => sketch.current?.addImg(imageUrl, { scale: 0.25 })}>
Add image
</button>
<button onClick={() => sketch.current?.addText("Hello", { fontSize: 24 })}>
Add text
</button>
<button
onClick={() =>
sketch.current?.setBackgroundFromDataUrl(imageUrl, { stretched: true })
}>
Background
</button>
<button onClick={download}>Download</button>
</>
);
};toDataURL() draws the whole drawing, whatever the zoom and the pan of the view, so a small copy of it,
drawn again after each change, is a preview (or a minimap) of the sketch:
import { useRef, useState } from "react";
import { SketchField } from "react-sketch";
export const WithPreview = () => {
const sketch = useRef<SketchField>(null);
const [preview, setPreview] = useState("");
const refresh = () =>
setPreview(sketch.current?.toDataURL({ multiplier: 0.25 }) ?? "");
return (
<>
<SketchField
ref={sketch}
onChange={refresh}
onObjectAdded={refresh}
onObjectRemoved={refresh}
/>
{preview && <img src={preview} alt="Preview of the drawing" />}
</>
);
};onChange is called for a drawing, undo, redo and the load of a value. onObjectAdded and
onObjectRemoved also tell of the objects which code adds or removes (addText(), addImg(), paste(),
removeSelected()). zoom() and clear() tell nothing, so draw the preview again after them. With
onViewChange, a frame on the preview can show what the canvas shows: the visible part starts at
-x / zoom, -y / zoom in canvas pixels, and is the size of the canvas divided by zoom. The showcase has
this in its Preview panel. toDataURL() throws for a canvas with an image of another site which does not
allow it (CORS), so catch it where that can happen.
- The background image is drawn under the objects. Set it with the
backgroundImageproperty (andbackgroundImageOptions), or withsetBackgroundFromDataUrlandremoveBackgroundImage. The property takes the place of the background image of avalue. - The overlay image is drawn above the objects, for example an outline to draw within. Set it with
setOverlayFromDataUrland remove it withremoveOverlayImage. It takes no mouse events, so the objects below can still be selected, andtoJSONandtoDataURLinclude it.clear()removes it, and so does loading avaluethat has none. - The background and the overlay images are requested with CORS (
crossOrigin: "anonymous"), so the canvas can be exported. A server without CORS headers cannot be used that way: passcrossOrigin: nullto show the image anyway, knowing that a cross-origin image taints the canvas andtoDataURLthrows a SecurityError.addImgrequests the image without CORS, as before, unless you passcrossOrigin.
sketch.current?.setOverlayFromDataUrl(outlineUrl, { stretched: true });
sketch.current?.addImg(photoUrl, { crossOrigin: "anonymous" }); // the canvas can still be exportedAll properties are optional.
| Property | Type | Default | Description |
|---|---|---|---|
tool |
Types.Tool or string |
"select" |
The tool to use, see Tools. null is treated as "select", an unknown name selects "pencil". |
lineColor |
string |
"black" |
Color of the line. |
lineWidth |
number |
10 |
Width of the line. |
lineStyle |
Types.LineStyle |
"solid" |
Style of the line: "solid", "dashed", "dotted" or "dash-dot". See Line style. |
fillColor |
string |
"transparent" |
Fill color of the shapes, where applicable (rectangle, rectangle-label, circle). |
backgroundColor |
string |
"transparent" |
Background color of the sketch, in hex or rgba format. |
backgroundImage |
string |
URL or data URL of the background image. It is set when the component mounts and when it changes, and removed when it is gone. It takes the place of the background image of value and defaultValue. |
|
backgroundImageOptions |
Types.BackgroundOptions |
Options of backgroundImage, those of setBackgroundFromDataUrl. Options written inline in a render do not load the image again when their content is the same. |
|
opacity |
number |
1.0 |
Accepted and not applied: it has no effect. It had no effect in 0.5.x either. |
undoSteps |
number |
25 |
Number of undo/redo steps to keep. Read when the component mounts, a later change has no effect. |
imageFormat |
string |
Default format of toDataURL, png or jpeg, used when the options of the call have no format. Without it, png. |
|
value |
object or string |
Drawing to load, loaded again when it changes. See Initial, loaded and saved drawings. | |
forceValue |
boolean |
false |
Load value on every update of the component, even when it did not change. |
defaultValue |
object or string |
Drawing loaded when the component mounts, unless value is set. |
|
widthCorrection |
number |
0 |
Pixels subtracted from the width of the container when the canvas is resized. Compensates a border or padding of the container. |
heightCorrection |
number |
0 |
Pixels subtracted from the height of the container when the canvas is resized. |
autoresize |
boolean |
true |
Resize the canvas, and scale its objects, when the window is resized. The canvas is always fitted to its container on mount and when width or height change. |
pinchZoom |
boolean |
true |
Zoom and pan the view of the canvas with two fingers on a touch screen. See Pinch zoom. |
minZoom |
number |
0.5 |
The smallest zoom of the view, 1 being the whole canvas. |
maxZoom |
number |
8 |
The largest zoom of the view. |
width |
number |
undefined | Width of the sketch in pixels. When not set, the sketch takes the width of its parent element. |
height |
number |
512 |
Height of the sketch in pixels. |
className |
string |
Class name of the container element of the canvas. | |
style |
CSS.Properties |
Style of the container element of the canvas. | |
onChange |
(event?: Event) => void |
Called 10 ms after the mouse is released on the canvas, or leaves it, when an object was added, changed or removed since it was pressed, and after undo, redo and the load of a value. It is not called for a click which only selects, a drag of the pan tool, or a pointer which comes and goes. The argument is the DOM event after a mouse event, nothing otherwise. |
|
onObjectAdded |
(event: Types.SketchEvent) => void |
no-op | An object was added: drawn, pasted, or loaded with a value. Not called when undo or redo add an object back. |
onObjectModified |
(event: Types.SketchEvent) => void |
no-op | An object was modified by the user (moved, scaled, rotated). |
onObjectRemoved |
(event: Types.SketchEvent) => void |
no-op | An object was removed from the sketch. |
onObjectMoving |
(event: Types.SketchEvent) => void |
no-op | An object is being moved. |
onObjectScaling |
(event: Types.SketchEvent) => void |
no-op | An object is being scaled. |
onObjectRotating |
(event: Types.SketchEvent) => void |
no-op | An object is being rotated. |
onMouseDown |
(event: Types.SketchEvent) => void |
no-op | Mouse down on the canvas. |
onMouseMove |
(event: Types.SketchEvent) => void |
no-op | Mouse move on the canvas. |
onMouseUp |
(event: Types.SketchEvent) => void |
no-op | Mouse up on the canvas. |
onMouseOut |
(event: Types.SketchEvent) => void |
no-op | The mouse left the canvas. |
onViewChange |
(view: Types.SketchView) => void |
The view changed (two fingers, or resetView()): zoom (1 is the whole canvas) and the origin x, y of the canvas, in pixels. |
Types.SketchEvent is { e?: Event; target?: Types.SketchObject; [key: string]: unknown }: e is the
DOM event, target is the object the event is about (object events only). Types.SketchObject is
{ type?: string; [key: string]: unknown }, for example with the type "path", "line" or "rect".
The methods are properties of the component instance, access them through a ref. They are valid from
the moment the component is mounted until it is unmounted. Calling one before the component is mounted
throws.
| Method | Returns | Description |
|---|---|---|
undo() |
void |
Undo the last action. Does nothing when there is nothing to undo. Calls onChange when it undid something. |
redo() |
void |
Redo the last undone action. Does nothing when there is nothing to redo. Calls onChange when it redid something. |
canUndo() |
boolean |
Whether undo() has something to undo. |
canRedo() |
boolean |
Whether redo() has something to redo. |
toDataURL(options?) |
string |
The sketch as a data URL. Options: format ("png" or "jpeg", default imageFormat, then "png"), quality (0 to 1, jpeg only, default 1), multiplier (default 1), and the crop area left, top, width, height. |
toJSON(propertiesToInclude?) |
Types.SketchData |
The sketch as an object. propertiesToInclude lists additional properties of the objects to include. |
fromJSON(json?) |
void |
Load a drawing (object or JSON string). Loads after 100 ms, starts the undo history again with the loaded objects, applies the tool configuration again and calls onChange. Does nothing for an empty argument. |
clear(propertiesToInclude?) |
Types.SketchData |
Remove all objects, the background image and the overlay image, and reset the undo history. Returns the discarded content as JSON. The backgroundColor property is kept. |
addImg(dataUrl, options?) |
void |
Add an image, given as URL or data URL, as an object. It is added when it has loaded. Options: left, top (default: random position), scale (default 0.5), crossOrigin (see Background and overlay images). |
addText(text, options?) |
void |
Add an editable text. It is centered unless left or top are given. options are the options of a fabric text object, for example fontSize, fill. |
setBackgroundFromDataUrl(dataUrl, options?) |
void |
Use an image as background. Options: stretched (fill the canvas, the proportions change), stretchedX (fit the width), stretchedY (fit the height), both are stretched; crossOrigin; the others are image options. |
removeBackgroundImage() |
void |
Remove the background image, also one that is still loading. The objects, the overlay image and the backgroundColor are kept. Like setBackgroundFromDataUrl, it is not part of the undo history. |
setOverlayFromDataUrl(dataUrl, options?) |
void |
Use an image as overlay, above the objects. It takes no mouse events, toJSON and toDataURL include it, and it is not part of the undo history. The options are those of setBackgroundFromDataUrl. |
removeOverlayImage() |
void |
Remove the overlay image, also one that is still loading. The objects, the background image and the backgroundColor are kept. |
zoom(factor) |
void |
Scale the objects, the background image and the overlay image by a factor: 2 doubles their size, 0.5 halves it. |
resetView() |
void |
Put the view back to the whole canvas, after it was zoomed or panned with two fingers. |
hasSelection() |
boolean |
Whether an object is selected. |
clearSelection() |
void |
Deselect the selected object or objects. |
removeSelected() |
void |
Remove the selected object or objects. It can be undone. |
copy() |
void |
Copy the selected object. Does nothing without a selection. |
paste() |
void |
Paste the copied object, 10 pixels further down and to the right of the previous one. Does nothing when nothing was copied. |
enableTouchScroll() |
void |
Allow the page to scroll on touch devices when touching the canvas. |
disableTouchScroll() |
void |
Do not scroll the page when touching the canvas. |
Use the tools by name ("pencil"), with Tools (Tools.Pencil) or with the enumeration
Types.Tool (Types.Tool.PENCIL).
| Tool | Tools |
Value | Description |
|---|---|---|---|
| Select | Tools.Select |
select |
Disables drawing. Objects can be selected, moved, scaled and rotated. The tools which draw a shape, and the pan tool, freeze the objects (also the ones added while they are in use), so a drag draws or pans. |
| Pencil | Tools.Pencil |
pencil |
Free drawing. |
| Line | Tools.Line |
line |
Straight lines. |
| Arrow | Tools.Arrow |
arrow |
Lines with an arrow head. |
| Rectangle | Tools.Rectangle |
rectangle |
Rectangles, filled with fillColor. |
| Rectangle label | Tools.RectangleLabel |
rectangle-label |
Rectangles with a text box above them. The text box follows the rectangle when it is moved or scaled. |
| Polygon | Tools.Polygon |
polygon |
Closed shapes of straight sides, filled with fillColor. Each click or tap is a vertex. See Polygon. |
| Circle | Tools.Circle |
circle |
Circles, filled with fillColor. The drag from the first to the second point is the diameter. |
| Pan | Tools.Pan |
pan |
Disables drawing. Dragging moves the whole canvas, useful after zooming (contributed by wmaillard). |
| Highlighter | Tools.Highlighter |
highlighter |
Free drawing in a semi-transparent color. lineColor must be a hex code or a color name; any other value falls back to the default color. |
| Eraser | Tools.Eraser |
eraser |
Erases parts of the objects, with the width of lineWidth. The cursor shows the size of the brush. The background color and the background image are not erased. See Eraser. |
| Laser pointer | Tools.LaserPointer |
laser-pointer |
A line that follows the pointer, fades out and disappears. It is not part of the drawing. See Laser pointer. |
| Default tool | Tools.DefaultTool |
default-tool |
Disables drawing and selection. Use it to show a drawing that is not edited. |
lineStyle sets how the line is drawn: "solid" (the default), "dashed", "dotted" or "dash-dot". The
dashes and the dots follow lineWidth (a line thinner than 2 pixels has the pattern of one of 2), so a
thick line has long dashes and wide dots.
<Sketch tool={Sketch.Tools.Rectangle} lineWidth={3} lineStyle="dashed" />The pencil, line, arrow, rectangle, rectangle label, circle and polygon draw it. The head of an arrow and the
text box of a rectangle label are not dashed, and the highlighter, the eraser and the laser pointer do not
draw it. A change of lineStyle is for the next drawing: what is drawn already keeps its style. The style is
saved by toJSON() and loaded by fromJSON() with the object (strokeDashArray and strokeLineCap), and
the polygon in progress is shown in it.
Each click or tap places a vertex of the polygon, where the pointer is released, and a line follows the pointer from the last vertex (with a finger, while it is down, so the vertex is where it is lifted). A polygon needs three vertices, and it is closed in one of three ways, which do nothing with fewer:
- a click or tap on the first vertex, within 10 pixels (20 with a finger), which is ringed when the pointer is near enough;
- a double click or double tap (two releases within 400 ms and 4 pixels, 10 with a finger), which closes with the vertices placed so far;
- the Enter key.
The Escape key cancels the polygon in progress. The keys are listened to on the document only while a polygon is in progress, and only when the focus is on the page or in the sketch: a button, a field or a dialog which has the focus keeps its keys. With more than one sketch on a page, the keys go to the one which was pressed last. A polygon in progress is dropped when the sketch is cleared, when a value is loaded, and when an undo or a redo is done.
The polygon in progress is drawn on the top canvas of fabric only. It is not an object of the sketch (it is
not in toJSON(), toDataURL() or the undo history) until it is closed. Then it is one fabric.Polygon
with the color (lineColor), the weight (lineWidth) and the fill (fillColor, transparent by default) of
the line, and it is one undo step: onObjectAdded is called once with the polygon as target, and
onChange is called once, not for each vertex. onMouseDown and onMouseUp are called for every click.
Switching to another tool closes a polygon which has three vertices and keeps it, and drops one with fewer.
Unmounting drops the polygon in progress. zoom() and the resize of the canvas scale the polygon in progress
with the objects. A polygon which crosses itself is drawn as it is.
Once it is closed the polygon is a shape like the others: the select tool moves, scales and rotates it as a
whole, the eraser cuts it, and toJSON() and fromJSON() save and load it. The vertices are the points of
the object, in its own space, which is centered on pathOffset. To get them in canvas coordinates:
const matrix = polygon.calcTransformMatrix();
const vertices = polygon.points.map(({ x, y }) =>
fabric.util.transformPoint(
new fabric.Point(x - polygon.pathOffset.x, y - polygon.pathOffset.y),
matrix
)
);Moving the vertices of a polygon which is closed is not supported.
On a touch screen, two fingers on the canvas zoom and pan its view: the point between the fingers stays under
them while the distance between them sets the zoom (between minZoom and maxZoom), and moving them together
pans. The canvas cannot be taken out of sight, a quarter of its smaller side stays in the window. Set
pinchZoom to false to turn it off, call resetView() to go back to the whole canvas, and use onViewChange
to show the zoom.
The view is the viewport of the canvas, the objects stay where they are. So it is not saved by toJSON(), not
an undo step, onChange is not called for it, and toDataURL() draws the whole drawing, not the part which
the view shows. This is different from zoom(factor), which scales the objects.
The second finger makes the first one a gesture of the view: what the first finger started is dropped, a line, a shape or a free stroke is not drawn and leaves no undo step, and a polygon in progress keeps its vertices and places none. Drawing with two fingers at once is not possible. The mouse is not used for this: on a desktop the Pan tool moves the view.
The eraser cuts the strokes it draws out of the objects it crosses: a line, a shape, an image or a text.
A stroke is one undo step. The erased parts are kept on the objects, so toJSON() saves them and value,
defaultValue and fromJSON() load them. An object is only cut where the eraser crosses it, the rest of
the sketch stays, and the background color and the background image are not erased.
The laser pointer points at something on the sketch: the line follows the pointer, gets thinner towards its
start and disappears about a second after it was drawn. It has the color (lineColor) and the weight
(lineWidth) of the line, and it glows in its color.
The line is drawn on the top canvas of fabric only, it is never an object of the sketch. It is not in
toJSON() or toDataURL(), it is not an undo step, onObjectAdded is not called for it, and onChange is
not called when it is drawn. Switching to another tool clears what is left of the line.
| Export | Description |
|---|---|
SketchField |
The component (named export). Also usable as a type, for the ref. |
Tools |
The tools by name: Select, Pencil, Line, Arrow, Rectangle, RectangleLabel, Polygon, Circle, Pan, DefaultTool, Highlighter, Eraser, LaserPointer. |
Types |
Namespace of the types: Tool (enumeration), Tools, LineStyle, SketchProperties, SketchEvent, SketchObject, SketchValue, SketchData, DataUrlOptions, ImageOptions, BackgroundOptions, TextOptions. |
| default export | The component, which also carries SketchField and Tools (as in 0.5.x). |
opacityis accepted and not applied.undoStepskeeps one step more than its value:undoSteps + 1actions can be undone.- With
forceValue, every update of the component (for example each re-render of the parent with new properties) loadsvalueagain.
- React:
^17.0.0 || ^18.0.0 || ^19.0.0(peer dependency). The test suite runs on React 19. - Browsers: browsers with HTML5 canvas support. No polyfills are included and Internet Explorer 11 is not supported.
- Server-side rendering has not been tested. Render the component in the browser only.
- Node.js: the package has no runtime requirement for Node.js. Developing in this repository requires Node.js 22.22.1 or later (
.nvmrchas 24) and Yarn 1. The CI runs the checks and the tests on Node.js 22, 24 and 26.
This is a Yarn 1 workspaces monorepo.
| Path | Description |
|---|---|
packages/react-sketch |
The library, published to npm as react-sketch. The build output is dist/index.js (UMD bundle, react and fabric are external) and dist/index.d.ts (type declarations). |
packages/eraser |
The eraser brush, from @erase2d/fabric (private, bundled into react-sketch). |
packages/laserpointer |
The laser pointer brush (private, bundled into react-sketch). |
packages/showcase |
Demo application (not published), live on GitHub Pages: a canvas with floating tools, properties and settings, and the defaultValue, value, addImg and backgroundImage scenarios. |
.github/workflows |
Continuous integration (ci.yml) and the deployment of the showcase (pages.yml). |
Requirements: Node.js 22.22.1 or later, Yarn 1.
git clone https://github.com/tbolis/react-sketch.git
cd react-sketch
yarn install
yarn startyarn start serves the showcase at http://localhost:23000. It uses the sources of the library, so the
library does not have to be built first.
| Command | Description |
|---|---|
yarn start |
Serve the showcase on port 23000. |
yarn build |
Build the library (packages/react-sketch/dist) and the showcase (packages/showcase/build). |
yarn test |
Run the tests of the library (Jest). yarn test:watch and yarn test:coverage are also available. |
yarn typecheck |
Type-check both packages. |
yarn lint |
Lint the TypeScript sources with ESLint. yarn lint:fix fixes what it can. |
yarn format |
Format the repository with Prettier. yarn format:check only checks. |
yarn clean |
Remove the build output. |
The tests run on React 19. REACT=18 yarn test runs them on React 18, and CI does both.
pages.yml builds the showcase and publishes packages/showcase/build to
https://tbolis.github.io/react-sketch/. It runs on every push to main that changes the library, the
@react-sketch/* packages, the showcase, the lockfile, .nvmrc or the workflow itself (not markdown), and
on demand from the Actions tab (Run workflow). The showcase imports the sources of the library, so a
change of the library is in the next deployment. The repository needs Settings > Pages > Build and
deployment > Source set to GitHub Actions, once.
1.0.0 is a rewrite in TypeScript which keeps the component API of 0.5.x: the properties, the methods,
the named exports SketchField and Tools, and the default export. It requires React 17 or later and
does not support Internet Explorer 11. A few behaviors differ deliberately and several bugs are fixed,
the changelog lists them.
See CHANGELOG.md.
Issues and pull requests are welcome at https://github.com/tbolis/react-sketch/issues. Before opening a
pull request run yarn lint, yarn typecheck, yarn test and yarn format:check. A pre-commit hook
(husky and lint-staged) runs ESLint and Prettier on the staged files. Participation in this project is
subject to the Code of Conduct.
MIT. See LICENSE.