Style 01 · drawn
Software that still looks like it was thought through on paper.
Pencil lines, ink ticks and a little wobble, on top of native inputs that behave exactly like the ones your users already know. Below is a whole settings screen made of nothing but drawn components. Click around, switch the theme, then take it apart.
Account
Make this sketchbook yours.
We only use it to reply.
- ENEnglish
- DEDeutsch
- UKУкраїнська
Dark mode lives in the top bar. How it works
Read the whole screen, every line
import {
Button,
Card,
CardBody,
CardFooter,
CardHeader,
Checkbox,
IconButton,
IconButtonGroup,
Link,
MenuButton,
NavItem,
Progress,
RadioGroup,
SegmentedControl,
Select,
Tabs,
TextField,
ThemeToggle,
} from '@driif/motiv/drawn';
import { useState } from 'react';
import { useTheme } from '../useTheme';
function Icon({ d }: { d: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} aria-hidden="true">
<path d={d} strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
const languages = [
{ value: 'en', label: 'English', code: 'EN' },
{ value: 'de', label: 'Deutsch', code: 'DE' },
{ value: 'uk', label: 'Українська', code: 'UK' },
];
const digests = [
{ value: 'daily', label: 'Every morning' },
{ value: 'weekly', label: 'Once a week' },
{ value: 'never', label: 'Never, thanks' },
];
const densities = [
{ value: 'cozy', label: 'Cozy' },
{ value: 'compact', label: 'Compact' },
];
export function DrawnSettings() {
const { dark, toggle } = useTheme();
const [menu, setMenu] = useState(false);
const [tab, setTab] = useState('profile');
const [language, setLanguage] = useState('en');
const [digest, setDigest] = useState('weekly');
const [density, setDensity] = useState('cozy');
const [name, setName] = useState('Ada Lovelace');
const [newsletter, setNewsletter] = useState(false);
const [saving, setSaving] = useState(false);
const done = [name, language, newsletter, digest !== 'never'].filter(Boolean).length;
return (
<div className="site-app" data-menu={menu}>
<header className="site-app__bar">
<MenuButton className="site-app__menu" open={menu} onToggle={() => setMenu(!menu)} />
<strong className="site-app__brand">sketchbook</strong>
<ThemeToggle dark={dark} onToggle={toggle} />
</header>
<nav className="site-app__nav" aria-label="Sketchbook">
<NavItem href="#settings" icon={<Icon d="M4 11l8-7 8 7v9h-5v-6H9v6H4z" />} label="Home" />
<NavItem href="#settings" icon={<Icon d="M4 6h16v12H4zM4 6l8 7 8-7" />} label="Inbox" />
<NavItem
href="#settings"
icon={<Icon d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" />}
label="Settings"
active
/>
</nav>
<Card as="section" className="site-app__main" aria-label="Account settings">
<CardHeader
title="Account"
titleAs="h2"
description="Make this sketchbook yours."
actions={
<IconButtonGroup label="Avatar">
<IconButton icon={<Icon d="M4 20l4-1 11-11-3-3L5 16l-1 4z" />} label="Edit avatar" />
<IconButton
icon={<Icon d="M5 7h14M9 7V4h6v3M7 7l1 13h8l1-13" />}
label="Remove avatar"
/>
</IconButtonGroup>
}
/>
<CardBody>
<Progress className="site-app__progress" value={done * 25} label="Profile complete" />
<Tabs
label="Settings"
value={tab}
onChange={setTab}
items={[
{
value: 'profile',
label: 'Profile',
panel: (
<div className="site-app__panel">
<TextField
label="Your name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<TextField label="Email" type="email" hint="We only use it to reply." />
<Select
label="Language"
options={languages}
value={language}
onChange={setLanguage}
/>
</div>
),
},
{
value: 'letters',
label: 'Letters',
panel: (
<div className="site-app__panel">
<Checkbox
label="Send me the newsletter"
checked={newsletter}
onChange={(event) => setNewsletter(event.target.checked)}
/>
<RadioGroup
name="digest"
legend="Activity digest"
options={digests}
value={digest}
onChange={setDigest}
/>
</div>
),
},
{
value: 'looks',
label: 'Looks',
panel: (
<div className="site-app__panel">
<SegmentedControl
label="Density"
options={densities}
value={density}
onChange={setDensity}
/>
<p className="site-app__note">
Dark mode lives in the top bar. <Link href="#theme-toggle">How it works</Link>
</p>
</div>
),
},
]}
/>
</CardBody>
<CardFooter>
<Button variant="ghost">Cancel</Button>
<Button
loading={saving}
onClick={() => {
setSaving(true);
setTimeout(() => setSaving(false), 1200);
}}
>
Save changes
</Button>
</CardFooter>
</Card>
</div>
);
}Getting started
Four steps from empty folder to sketchbook.
Install
React 19 and react-dom are the only peers.
pnpm add @driif/motivImport the stylesheet once
It carries the tokens, a small base and the Caveat font. Use it instead of
styles.css, never next to it.import '@driif/motiv/drawn.css';Draw your first form
Every component is a named export. No provider, no setup.
import '@driif/motiv/drawn.css'; import { Button, TextField } from '@driif/motiv/drawn'; export function Subscribe() { return ( <form> <TextField label="Email" type="email" hint="We only use it to reply." /> <Button type="submit">Subscribe</Button> </form> ); }Own the theme
The toggles only report clicks. Your app sets
data-themeon<html>; this is the hook this site uses.import { useSyncExternalStore } from 'react'; // The library's toggles only report clicks; the app owns <html data-theme>. // Every toggle on the page reads the attribute, so they all stay in step. function subscribe(onChange: () => void) { const observer = new MutationObserver(onChange); observer.observe(document.documentElement, { attributeFilter: ['data-theme'] }); return () => observer.disconnect(); } export function useTheme() { const dark = useSyncExternalStore( subscribe, () => document.documentElement.dataset.theme === 'dark', () => false, ); const toggle = () => { const theme = dark ? 'light' : 'dark'; document.documentElement.dataset.theme = theme; localStorage.setItem('motiv-site-theme', theme); }; return { dark, toggle }; }
Button
The one thing on the page that should be pressed, cut by hand. Ghost buttons get a wavy stroke; links borrow the look.
import { Button, buttonClassName } from '@driif/motiv/drawn';
export function ButtonDemo() {
return (
<>
<Button>Save</Button>
<Button variant="outline">Preview</Button>
<Button variant="ghost">Cancel</Button>
<Button loading>Publishing</Button>
<a href="#card" className={buttonClassName({ variant: 'secondary' })}>
A link, dressed up
</a>
</>
);
}InkSplatterButton
A button that splashes ink when pressed, with an optional success label for a moment.
import { InkSplatterButton } from '@driif/motiv/drawn';
export function InkSplatterButtonDemo() {
return (
<InkSplatterButton
successLabel="Liked"
icon={
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
aria-hidden="true"
>
<path
d="M12 20s-7-4.5-7-10a4 4 0 0 1 7-2.5A4 4 0 0 1 19 10c0 5.5-7 10-7 10z"
strokeLinejoin="round"
/>
</svg>
}
>
Like
</InkSplatterButton>
);
}HoldToConfirmButton
For things that are hard to undo: hold with pointer, Space or Enter until the ink fills; a screen-reader click confirms directly.
import { HoldToConfirmButton } from '@driif/motiv/drawn';
import { useState } from 'react';
export function HoldToConfirmButtonDemo() {
const [count, setCount] = useState(0);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<HoldToConfirmButton
label="Hold to seal"
confirmedLabel="Sealed"
onConfirm={() => setCount(count + 1)}
/>
<span>Sealed {count} times</span>
</div>
);
}Card
A hand-cut sheet of paper with a heading, some words and a place for actions.
Account
Make this sketchbook yours.
import {
Button,
Card,
CardBody,
CardFooter,
CardHeader,
IconButton,
IconButtonGroup,
} from '@driif/motiv/drawn';
function Icon({ d }: { d: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} aria-hidden="true">
<path d={d} strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function CardDemo() {
return (
<Card style={{ maxWidth: '28rem' }}>
<CardHeader
title="Account"
description="Make this sketchbook yours."
actions={
<IconButtonGroup label="Account actions">
<IconButton icon={<Icon d="M4 20l4-1 11-11-3-3L5 16l-1 4z" />} label="Edit" />
<IconButton icon={<Icon d="M5 7h14M9 7V4h6v3M7 7l1 13h8l1-13" />} label="Delete" />
</IconButtonGroup>
}
/>
<CardBody>Three new sketches, one finished page, a lot of coffee.</CardBody>
<CardFooter>
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button variant="outline" size="sm">
Save changes
</Button>
</CardFooter>
</Card>
);
}TextField
A pencil line to write on. Focus draws it in ink, the label floats, the hint stays put.
We only use it to reply.
import { TextField } from '@driif/motiv/drawn';
export function TextFieldDemo() {
return (
<>
<TextField label="Your name" defaultValue="Ada Lovelace" />
<TextField label="Email" type="email" hint="We only use it to reply." />
</>
);
}Checkbox
A native checkbox, ticked by hand.
import { Checkbox } from '@driif/motiv/drawn';
export function CheckboxDemo() {
return (
<>
<Checkbox label="Send me the newsletter" defaultChecked />
<Checkbox label="Also the long one, on Sundays" />
</>
);
}Radio & RadioGroup
One choice out of a few. RadioGroup wraps the fieldset and legend for you.
import { RadioGroup } from '@driif/motiv/drawn';
import { useState } from 'react';
const options = [
{ value: 'tea', label: 'Tea' },
{ value: 'coffee', label: 'Coffee' },
{ value: 'water', label: 'Water' },
];
export function RadioDemo() {
const [drink, setDrink] = useState('tea');
return (
<RadioGroup name="drink" legend="Drink" options={options} value={drink} onChange={setDrink} />
);
}Select
A listbox with codes and hints, for when there are too many radios.
- ENEnglish
- DEDeutsch
- UKУкраїнська
import { Select } from '@driif/motiv/drawn';
import { useState } from 'react';
const options = [
{ value: 'en', label: 'English', code: 'EN', hint: 'Default language' },
{ value: 'de', label: 'Deutsch', code: 'DE', hint: 'German' },
{ value: 'uk', label: 'Українська', code: 'UK', hint: 'Ukrainian' },
];
export function SelectDemo() {
const [language, setLanguage] = useState('');
return <Select label="Language" options={options} value={language} onChange={setLanguage} />;
}SegmentedControl
Two to four mutually exclusive views, side by side.
import { SegmentedControl } from '@driif/motiv/drawn';
import { useState } from 'react';
const options = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
];
export function SegmentedControlDemo() {
const [range, setRange] = useState('week');
return <SegmentedControl label="Range" options={options} value={range} onChange={setRange} />;
}Tabs
Panels behind labels, with arrow-key navigation.
import { Tabs } from '@driif/motiv/drawn';
import { useState } from 'react';
const items = [
{ value: 'overview', label: 'Overview', panel: 'A quick look at everything.' },
{ value: 'activity', label: 'Activity', panel: 'What happened recently.' },
{ value: 'settings', label: 'Settings', panel: 'Tune it to taste.' },
];
export function TabsDemo() {
const [tab, setTab] = useState('overview');
return <Tabs label="Sections" items={items} value={tab} onChange={setTab} />;
}Link
An underline drawn in ink, with an arrow when it leads somewhere.
import { Link } from '@driif/motiv/drawn';
export function LinkDemo() {
return (
<>
<Link href="#getting-started">Back to the start</Link>
<Link href="https://github.com/driif/motiv" external>
Source on GitHub
</Link>
</>
);
}IconButton & IconButtonGroup
A toolbar of sketched icons, each with a label for screen readers.
import { IconButton, IconButtonGroup } from '@driif/motiv/drawn';
function Icon({ d }: { d: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} aria-hidden="true">
<path d={d} strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function IconButtonDemo() {
return (
<IconButtonGroup label="Tools">
<IconButton icon={<Icon d="M4 20l4-1 11-11-3-3L5 16l-1 4z" />} label="Edit" />
<IconButton icon={<Icon d="M12 5v14M5 12h14" />} label="Add" />
<IconButton icon={<Icon d="M5 7h14M9 7V4h6v3M7 7l1 13h8l1-13" />} label="Delete" />
</IconButtonGroup>
);
}Tooltip
A margin note behind an i. Hover, focus or click to read it, Escape to put it away.
Delivery takes three daysLeave a little room for the unexpected.
import { Tooltip } from '@driif/motiv/drawn';
export function TooltipDemo() {
return (
<p style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '5rem' }}>
Delivery takes three days
<Tooltip content="Leave a little room for the unexpected." label="About delivery" />
</p>
);
}Progress
How far along something is, filled in with a marker.
import { Progress } from '@driif/motiv/drawn';
export function ProgressDemo() {
return (
<>
<Progress value={62} label="Uploading" />
<Progress value={100} label="Done" />
</>
);
}ThemeToggle
A compact sun and moon. It reports clicks; your app sets data-theme.
import { ThemeToggle } from '@driif/motiv/drawn';
import { useTheme } from '../../useTheme';
export function ThemeToggleDemo() {
const { dark, toggle } = useTheme();
return <ThemeToggle dark={dark} onToggle={toggle} />;
}OrbitalThemeToggle
The theme switch as a small illustration, for a hero or a settings page.
import { OrbitalThemeToggle } from '@driif/motiv/drawn';
import { useTheme } from '../../useTheme';
export function OrbitalThemeToggleDemo() {
const { dark, toggle } = useTheme();
return <OrbitalThemeToggle dark={dark} onToggle={toggle} />;
}