npm i @eduportdesign/tokens @eduportdesign/web-components
Use it in your project
These steps work for a new app or an existing one. Components are Lit web components, so they run in any framework or none. React apps get wrappers with typed props and events. Everything is on npm:
@eduportdesign/tokens: colors, type, spacing, radius, shadow and motion as CSS variables, SCSS and JS.@eduportdesign/web-components: the components, as custom elements like<ep-button>.@eduportdesign/react: React wrappers like<Button>.
1. Install the packages
# Plain HTML, Vue, Svelte, Angular or any other framework
npm install @eduportdesign/tokens @eduportdesign/web-components
# React (the wrappers bring the web components with them)
npm install @eduportdesign/tokens @eduportdesign/react
# The Onest and Gabarito fonts (recommended; without them text falls back to the system font)
npm install @fontsource-variable/onest @fontsource-variable/gabarito
2. Load the CSS once
Import these at your app's entry point (main.js, main.tsx or similar), before any
component renders. Without tokens.css the components have no colors.
import '@fontsource-variable/onest';
import '@fontsource-variable/gabarito';
import '@eduportdesign/tokens/css'; // required: light theme, Clay palette, soft corners
import '@eduportdesign/tokens/css/theme-dark'; // optional: dark theme
import '@eduportdesign/tokens/css/palette-sage'; // optional: Sage & Sand palette
import '@eduportdesign/tokens/css/corners-sharp'; // optional: sharp corners
import '@eduportdesign/tokens/css/base'; // recommended: body, headings, text, links, focus
The base stylesheet styles the page itself (background, text, headings, links and the focus ring) so
everything outside the components matches them. It resets heading and paragraph margins, so space your
layout with gap, and any rule in your own CSS overrides it.
3. Pick a theme, palette and corner style
Set data-theme (light or dark), data-palette
(clay or sage) and data-corners (soft or
sharp) on the root element. All are optional, default to light, Clay and soft, and can change
at runtime. The Sage & Sand palette needs palette-sage loaded.
<html data-theme="dark" data-palette="sage" data-corners="sharp">
To follow the operating system's light or dark setting:
const dark = matchMedia('(prefers-color-scheme: dark)');
const apply = () => (document.documentElement.dataset.theme = dark.matches ? 'dark' : 'light');
apply();
dark.addEventListener('change', apply);
4. Use the components
Plain HTML or JavaScript (with Vite or any bundler)
// main.js
import '@eduportdesign/web-components'; // every component
import '@eduportdesign/web-components/components/button'; // or only the ones you use
<!-- index.html -->
<ep-text-field label="Email" name="email" type="email" required></ep-text-field>
<ep-button variant="primary" type="submit">Sign up</ep-button>
Attributes are kebab-case (helper-text); the matching JS properties are camelCase
(helperText). Events bubble, so read new values from event.target. Form controls
work inside a native <form> with no extra wiring.
No build step
For a prototype or a static page, load everything from the jsDelivr CDN:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eduportdesign/tokens@0.7.0/dist/css/tokens.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@eduportdesign/web-components@0.7.0/+esm"></script>
<ep-button variant="primary">Save</ep-button>
React
Component names drop the prefix and use PascalCase: ep-text-field becomes TextField.
import { useState } from 'react';
import { Button, TextField } from '@eduportdesign/react';
export function SignUp() {
const [email, setEmail] = useState('');
return (
<form onSubmit={(e) => { e.preventDefault(); save(email); }}>
<TextField label="Email" type="email" value={email} onInput={(e) => setEmail(e.target.value)} />
<Button variant="primary" type="submit">Sign up</Button>
</form>
);
}
In Next.js, put 'use client' at the top of files that use the components.
Vue
Tell Vue that ep- tags are custom elements, then use them in templates as in plain HTML.
// vite.config.js
import vue from '@vitejs/plugin-vue';
export default {
plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('ep-') } } })],
};
Angular
Add the CSS to styles in angular.json, import the components in
main.ts, and allow custom elements in each component that uses them.
// angular.json → projects.<app>.architect.build.options
"styles": ["node_modules/@eduportdesign/tokens/dist/css/tokens.css", "src/styles.css"]
// main.ts
import '@eduportdesign/web-components';
// any.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({ standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: '<ep-button>Save</ep-button>' })
export class AnyComponent {}
Svelte
No setup beyond steps 1 to 3. Import the components in main.js and use the tags directly.
5. Style your own UI with the tokens
Use the semantic --ep-* variables instead of hex values, so your screens switch theme and corner
style along with the components.
.card {
background: var(--ep-color-surface-raised);
color: var(--ep-color-text-primary);
border-radius: var(--ep-radius-container);
padding: var(--ep-space-300);
}
Adding it to an existing app
- Import only the components you need, one path at a time, to keep bundles small.
-
The theme attributes work on any element, so you can adopt it one screen at a time with
<section data-theme="light">. - The tokens are namespaced
--ep-*and the tagsep-*, so they won't clash with your existing styles.
Next steps
Browse every component below, or every prop, slot and event in Storybook. The README covers installing from GitHub Packages. If you use an AI coding assistant, point it at docs/llm-guide.md.
A dashboard, built from the system
Nothing here is custom: the rail, hero, cards, covers, rings, sparklines and avatars are the published components. Switch the theme or corners at the top and the whole screen follows.
Today
- Physics 11 · OpticsLab 2
- Mathematics 10 · QuadraticsRoom 14
- Chemistry 12 · TitrationLab 1
Submissions this week
Example screen
A few components working together, the way they might in a learning app.
Physics for Class 11
Ask a doubt
Color
--ep-color-*Two palettes for the Signal style: Clay (coral on warm clay, the default) and Sage & Sand. Each has one key color and its own neutrals, and both share six category colors that tell things apart without saying good or bad. Switch palettes at the top of the page. Components only read the semantic tokens, which change with the theme. Each swatch shows the text color that passes WCAG AA on it, and its contrast ratio.
Key color · Coral
Neutral · Clay
Category colors subtle · bold · text, per tone
Typography
--ep-font-*
Onest for everything you read, Gabarito for the big headings, JetBrains Mono for code. Use the
role tokens (--ep-font-heading-lg, --ep-font-body-md) instead of picking a size and weight.
Elevation
--ep-shadow-*Surfaces rise in steps: the canvas, the sheet on it, raised cards on the sheet, then overlays that float above everything. In dark mode the surface color does most of the lifting, with a brighter edge instead of a bigger shadow.
Spacing
--ep-space-*Every margin, padding and gap is a step on this 4px-based scale. Small steps are for the inside of components; page layout uses multiples of 8px. Full rules in docs/layout.md.
| Token | Value | Size | Use for |
|---|
Layout grid
--ep-layout-*Pages sit on a 4, 8 or 12 column grid. The columns, page margin, gutter and section gap change at each breakpoint by themselves, so a page written once adapts. Resize the window to watch this one change.
| Breakpoint | Min width | Columns | Margin | Gutter | Section gap | Header |
|---|---|---|---|---|---|---|
| base | 0 | 4 | 16px | 16px | 32px | 56px |
md | 600px | 8 | 24px | 24px | 40px | 64px |
lg | 1024px | 12 | 32px | 24px | 48px | 64px |
xl | 1440px | 12 | 40px | 32px | 48px | 64px |
Page structure
Most screens share one shell: a top bar, side navigation from lg (a drawer below it), then a page header and
sections. The wireframe below responds to its own width with a container query, the way a reusable layout should.
header-heightsidebar-widthMotion and timing
--ep-motion-* data-motion
Interactions are fast, quiet and predictable. Motion only explains a change: a state changing, something
entering or leaving, where something moved, or that work is in progress. Nothing in a component runs longer than
300ms, only opacity and transform animate, and reduced motion keeps the fades but drops the movement.
Turn on Reduce motion below to compare. Full policy in
docs/motion.md.
Sets data-motion="reduced" on the page, the way an app's own setting would. Your operating system's reduced-motion setting applies too, and this can't turn it off.
| Token | Default | Reduced | Use for |
|---|---|---|---|
--ep-motion-duration-fast | 100ms | 100ms | Color, border and opacity changes; tooltips |
--ep-motion-duration-normal | 200ms | 100ms | Modals entering; anything leaving |
--ep-motion-duration-slow | 300ms | 100ms | Toasts entering; the most any component uses |
--ep-motion-duration-slower | 400ms | 100ms | Drawers and page-level panels only |
--ep-motion-duration-move | 200ms | 0ms | Movement inside a control: switch thumb, tab indicator |
--ep-motion-distance-sm | 4px | 0 | Tooltip, menu and popover enter offset |
--ep-motion-distance-md | 8px | 0 | Toast enter offset |
--ep-motion-scale-enter | 0.98 | 1 | Modal enter scale |
Hover waits 500ms. Once one is open, slide along the row and the next opens at once. Tab to them and they open with no wait.
The quick one finishes in 250ms, so no spinner flashes. The slow one shows a spinner after 400ms. Extra presses while busy are ignored.
Toasts stay 6 seconds, plus 1 second for every 20 words over 20. A toast with an action stays until you dismiss it. Hover or focus pauses the timer.
- Shake on error, ripple, press scale-down or hover lift
- Bounce, spring or overshoot easing
- Animated number counters, typewriter text, parallax, auto-playing carousels
- Animated or pulsing focus rings
Button
<ep-button>Starts an action. Use one primary button per view. Works with forms through type="submit" and type="reset".
Icon button
<ep-icon-button>A square button with only an icon. The label is required and is what screen readers announce. Hover or focus for the tooltip.
Text field
<ep-text-field>A single-line input with a label, helper text and error message. Takes part in forms like a native input.
Textarea
<ep-textarea>Multi-line text. Set maxlength to show a character count.
Select
<ep-select>Picks one option from a list. Built on the native select, so it uses the phone's own picker and works with every screen reader.
Checkbox
<ep-checkbox>Turns one option on or off, or picks several from a list. Supports an indeterminate state for "select all".
Radio group
<ep-radio-group> <ep-radio>Picks exactly one option. Tab moves into the group, arrow keys move between options.
Switch
<ep-switch>A setting that takes effect straight away.
Date picker
<ep-date-picker> <ep-calendar>Type a date in your locale's order or pick it from the calendar. The value is always YYYY-MM-DD. In the calendar, arrows move by day and week, and Page Up and Page Down change the month.
Badge
<ep-badge>A short, non-interactive status or count. The text always says what the status is, so it never relies on color.
Tag
<ep-tag>A keyword, category or active filter. Removable tags have their own button, so they work by keyboard.
Avatar
<ep-avatar>A person's picture, falling back to initials and then an icon. Always round, even with sharp corners.
Card
<ep-card>Groups related content. Three styles: outlined, elevated and filled. Media, header actions and footer are optional.
Outlined
The default. A border separates it from the page.Elevated
Filled
A tinted background with no border. Good for quiet, secondary groups.Cover art
<ep-cover>
Generated artwork for course tiles and empty states, in a category color. The same seed always
draws the same picture, so give each course its own. It is decorative and hidden from screen readers.
Stat
<ep-stat>One headline number with its label and an optional note or chart. Use a few in a row at the top of a dashboard.
Sparkline
<ep-sparkline>A trend with no axes, as a line, a line with an area, or bars. The last value is marked. Always give it a label.
Progress ring
<ep-progress-ring>A circular completion indicator with the percentage in the middle. Always determinate.
Avatar group
<ep-avatar-group>Overlapping avatars that collapse into a count past max.
Icon tile
<ep-icon-tile>A rounded square holding an icon in a category tone: the identity mark for a subject, a team or a calendar.
Table
<ep-table>Styles a native <table> you write, so any cell content works. A button in a header plus aria-sort makes a sortable column; aria-selected on a row highlights it. It scrolls sideways on narrow screens.
| Fees | Actions |
|---|
| Period | Monday | Tuesday | Wednesday |
|---|---|---|---|
| 1 | Physics | Maths | Chemistry |
| 2 | Maths | English | Biology |
| 3 | Chemistry | Physics | Maths |
| 4 | English | Biology | Physics |
Icon
<ep-icon>Icons from Tabler (over 5,000 available). They size to the text and use its color. The components ship with the set below; apps can register any other Tabler icon.
Accordion
<ep-accordion> <ep-accordion-item>Collapsible sections for long pages where people look for one part, like an FAQ. Up and Down arrows move between headers.
Empty state
<ep-empty-state>What to show when a list or page has nothing in it yet. Say why it's empty and offer the next step.
Skeleton
<ep-skeleton>Placeholders in the shape of content that is loading, so the page doesn't jump. Mark the loading region with aria-busy="true".
App shell
<ep-app-shell> <ep-top-bar> <ep-side-nav> <ep-page-header>
The frame every screen sits in: a sticky top bar, the side navigation and the content area, spaced as the
page structure describes. From 1024px the navigation sits beside the content;
below that it moves into a drawer that the top bar's menu button opens. The shell adds the skip link, the
<main> landmark, the page margin and the content width cap. See it full screen in
Storybook.
<ep-app-shell>
<ep-top-bar slot="top-bar">
<a slot="brand" href="/">Eduport Teach</a>
<ep-icon-button slot="actions" icon="bell" label="Notifications"></ep-icon-button>
</ep-top-bar>
<ep-side-nav slot="nav">
<ep-side-nav-item href="/" icon="home">Home</ep-side-nav-item>
<ep-side-nav-item href="/courses" icon="book" current>Courses</ep-side-nav-item>
</ep-side-nav>
<ep-page-header heading="Courses">
Everything you teach this term.
<ep-button slot="actions">New course</ep-button>
</ep-page-header>
<!-- sections -->
</ep-app-shell>
Tabs
<ep-tabs> <ep-tab> <ep-tab-panel>Switches between views in the same place. Arrow keys move between tabs, Home and End jump to the ends.
Breadcrumbs
<ep-breadcrumbs> <ep-breadcrumb-item>Where the page sits in the app, with a link to each level above. The last item is the current page.
Pagination
<ep-pagination>Moves between pages of a long list. Listen for change and load the page in page. In narrow containers the numbers collapse to "Page 4 of 12".
Alert
<ep-alert>An inline message about a page or section. Each variant has its own icon, so meaning never depends on color alone.
Toast
toast() <ep-toast>A brief notification in the corner of the screen. Screen readers announce it, and its timer pauses while you hover or focus it.
Progress bar
<ep-progress-bar>How far a task has got. Leave out value when the amount of work isn't known.
Spinner
<ep-spinner>A short wait of unknown length. Set label="" when nearby text already says what's happening.
Tooltip
<ep-tooltip>A short label on hover or focus. Escape closes it, and you can move the pointer onto it without it disappearing.
Popover
<ep-popover>Interactive content anchored to a button, like a filter form. It doesn't block the page; clicking outside or pressing Escape closes it.
Modal
<ep-modal>A dialog for a focused task or a decision. Focus stays inside while it's open and goes back to the button when it closes.
Logos
@eduportdesign/logosEvery Eduport logo as an SVG file. Start with the orange wordmark on light backgrounds and the white one on orange or dark backgrounds. Keep clear space of half the wordmark's height around it, don't go below 80 px wide (24 px for the symbol), and never recolor, stretch or add effects. Each file can be downloaded.