Author SHA1 Message Date
Aksel Mini fea90c95c8 💄 Match error text color to the danger profile
Use the profile red directly for error messages so text and border
always match, even if the danger token is overridden per theme.
2026-09-05 14:20:00 +02:00
Aksel Mini 885df0a2ff 🐛 Strengthen TextField error border
Widen the error ring to 2px so invalid fields are visible at a glance
in dense forms.
2026-09-05 11:45:00 +02:00
Aksel Mini f9489fdffc 🔖 Release 0.2.0
Bump all packages to 0.2.0 and add CHANGELOG.md files documenting the
Accordion/Modal additions and the Button focus-ring fix.
2026-09-01 09:00:00 +02:00
Aksel Mini 0cedb5320f 🐛 Fix focus ring on Button
Replace the outline-based focus style with a box-shadow ring.
Safari clips outlines on flex children, leaving the focus indicator
partially invisible.
2026-08-28 08:50:00 +02:00
Aksel Mini 9c8f55f5cf ✨ Add aksel.nav.no website with live component examples
Vite + React docs site that imports @navikt/ds-react, @navikt/ds-css and
@navikt/ds-tokens from the workspace and renders every component.
Heading level now accepts string literals to match JSX usage.
2026-08-25 10:05:00 +02:00
Aksel Mini adfcead7be ✨ Add Accordion and Modal components
Accordion.Item manages its own open state with aria-expanded wiring.
Modal renders through a portal with backdrop click handling. Also adds
the ChevronDown icon used by the accordion header.
2026-08-21 15:10:00 +02:00
32 changed files with 1686 additions and 11 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="no">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Aksel Mini — Komponenter</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{
"name": "website",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit -p tsconfig.json && vite build",
"preview": "vite preview"
},
"dependencies": {
"@navikt/ds-css": "workspace:*",
"@navikt/ds-react": "workspace:*",
"@navikt/ds-tokens": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.2",
"typescript": "5.5.4",
"vite": "5.4.8"
}
}
+110
View File
@@ -0,0 +1,110 @@
import { useState } from "react";
import {
Accordion,
Alert,
BodyShort,
Button,
Heading,
Modal,
TextField,
} from "@navikt/ds-react";
export function App() {
const [modalOpen, setModalOpen] = useState(false);
return (
<main className="page">
<header className="intro">
<Heading level="1" size="xlarge">
Aksel Mini
</Heading>
<BodyShort size="medium">
Et miniatyr-designsystem som speiler strukturen til Aksel. Hver komponent
under er rendret direkte fra @navikt/ds-react med stiler fra @navikt/ds-css.
</BodyShort>
</header>
<section className="example">
<Heading level="2" size="medium">
Button
</Heading>
<div className="example__preview">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="danger">Danger</Button>
<Button size="small">Small</Button>
</div>
</section>
<section className="example">
<Heading level="2" size="medium">
Typography
</Heading>
<div className="example__preview example__preview--column">
<Heading level="3" size="small">
Heading small
</Heading>
<BodyShort size="medium">BodyShort medium — brukes til vanlig brødtekst.</BodyShort>
<BodyShort size="small">BodyShort small — brukes til hjelpetekst og metadata.</BodyShort>
</div>
</section>
<section className="example">
<Heading level="2" size="medium">
TextField
</Heading>
<div className="example__preview example__preview--column">
<TextField label="Navn" description="For- og etternavn" />
<TextField label="E-post" error="E-postadressen ser ikke gyldig ut" />
</div>
</section>
<section className="example">
<Heading level="2" size="medium">
Alert
</Heading>
<div className="example__preview example__preview--column">
<Alert variant="info">Søknaden din er mottatt.</Alert>
<Alert variant="success">Alt gikk bra.</Alert>
<Alert variant="warning">Fristen nærmer seg.</Alert>
<Alert variant="error">Noe gikk galt.</Alert>
</div>
</section>
<section className="example">
<Heading level="2" size="medium">
Accordion
</Heading>
<Accordion>
<Accordion.Item title="Hvordan søker jeg?">
<BodyShort>Du søker ved å fylle ut skjemaet digitalt.</BodyShort>
</Accordion.Item>
<Accordion.Item title="Hva koster det?" defaultOpen>
<BodyShort>Det er gratis å søke.</BodyShort>
</Accordion.Item>
</Accordion>
</section>
<section className="example">
<Heading level="2" size="medium">
Modal
</Heading>
<div className="example__preview">
<Button variant="secondary" onClick={() => setModalOpen(true)}>
Åpne modal
</Button>
</div>
<Modal open={modalOpen} onClose={() => setModalOpen(false)} label="Eksempelmodal">
<Heading level="3" size="small">
Bekreftelse
</Heading>
<BodyShort>Dette er en modal renderet via en portal.</BodyShort>
<Button variant="primary" onClick={() => setModalOpen(false)}>
Lukk
</Button>
</Modal>
</section>
</main>
);
}
+46
View File
@@ -0,0 +1,46 @@
:root {
color-scheme: light;
}
body {
margin: 0;
background-color: var(--a-bg-default);
color: var(--a-text-default);
font-family: var(--a-font-family);
}
.page {
max-width: 48rem;
margin: 0 auto;
padding: var(--a-spacing-8) var(--a-spacing-4);
display: flex;
flex-direction: column;
gap: var(--a-spacing-8);
}
.intro {
display: flex;
flex-direction: column;
gap: var(--a-spacing-2);
}
.example {
display: flex;
flex-direction: column;
gap: var(--a-spacing-4);
padding: var(--a-spacing-6);
border: 1px solid var(--a-border-subtle);
border-radius: var(--a-border-radius-large);
}
.example__preview {
display: flex;
flex-wrap: wrap;
gap: var(--a-spacing-3);
align-items: center;
}
.example__preview--column {
flex-direction: column;
align-items: stretch;
}
+12
View File
@@ -0,0 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "@navikt/ds-tokens/dist/tokens.css";
import "@navikt/ds-css/dist/index.css";
import "./main.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"]
},
"include": ["src", "vite.config.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
},
});
+13
View File
@@ -0,0 +1,13 @@
# Changelog
## Unreleased
- :bug: Strengthen TextField error styling with a wider error ring and matching error text color.
## 0.2.0
- :bug: Fix focus ring on Button — use a box-shadow ring so the indicator renders correctly in Safari.
## 0.1.0
- Initial release with styles for Button, BodyShort, Heading, TextField, Alert, Accordion and Modal.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "0.1.0",
"version": "0.2.0",
"description": "Component styles for Aksel Mini, built on @navikt/ds-tokens",
"license": "MIT",
"type": "module",
+53
View File
@@ -0,0 +1,53 @@
/**
* Accordion
*/
.navds-accordion {
border: 1px solid var(--a-border-subtle);
border-radius: var(--a-border-radius-medium);
overflow: hidden;
font-family: var(--a-font-family);
}
.navds-accordion__item + .navds-accordion__item {
border-top: 1px solid var(--a-border-subtle);
}
.navds-accordion__header {
display: flex;
align-items: center;
gap: var(--a-spacing-2);
width: 100%;
padding: var(--a-spacing-3) var(--a-spacing-4);
border: none;
background-color: var(--a-bg-default);
color: var(--a-text-default);
font-size: var(--a-font-size-heading-xsmall);
font-weight: var(--a-font-weight-bold);
text-align: left;
cursor: pointer;
}
.navds-accordion__header:hover {
background-color: var(--a-bg-subtle);
}
.navds-accordion__header:focus-visible {
outline: 2px solid var(--a-border-focus);
outline-offset: -2px;
}
.navds-accordion__chevron {
flex-shrink: 0;
transition: transform 150ms ease;
}
.navds-accordion__item--open .navds-accordion__chevron {
transform: rotate(180deg);
}
.navds-accordion__content {
padding: var(--a-spacing-3) var(--a-spacing-4) var(--a-spacing-4);
border-top: 1px solid var(--a-border-subtle);
color: var(--a-text-default);
}
+4 -2
View File
@@ -72,9 +72,11 @@
outline: none;
}
/* Use a box-shadow ring instead of outline: Safari clips outlines
on children with overflow in flex containers. */
.navds-button:focus-visible {
outline: 2px solid var(--a-border-focus);
outline-offset: 2px;
outline: none;
box-shadow: 0 0 0 3px var(--a-border-focus);
}
.navds-button:disabled,
+2
View File
@@ -4,3 +4,5 @@
@import "./button.css";
@import "./textfield.css";
@import "./alert.css";
@import "./accordion.css";
@import "./modal.css";
+49
View File
@@ -0,0 +1,49 @@
/**
* Modal
*/
.navds-modal__overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: var(--a-spacing-4);
background-color: rgb(35 38 42 / 60%);
}
.navds-modal {
position: relative;
width: 100%;
max-width: 30rem;
max-height: calc(100vh - 2 * var(--a-spacing-8));
overflow-y: auto;
padding: var(--a-spacing-6);
border-radius: var(--a-border-radius-large);
background-color: var(--a-bg-default);
color: var(--a-text-default);
font-family: var(--a-font-family);
}
.navds-modal__close {
position: absolute;
top: var(--a-spacing-3);
right: var(--a-spacing-3);
display: inline-flex;
padding: var(--a-spacing-2);
border: none;
border-radius: var(--a-border-radius-medium);
background-color: transparent;
color: var(--a-icon-default);
cursor: pointer;
}
.navds-modal__close:hover {
background-color: var(--a-bg-subtle);
}
.navds-modal__close:focus-visible {
outline: 2px solid var(--a-border-focus);
outline-offset: 2px;
}
+7 -1
View File
@@ -51,6 +51,12 @@
box-shadow: 0 0 0 1px var(--a-border-danger);
}
/* Increase the error ring so it reads at a glance on light backgrounds. */
.navds-text-field--error {
box-shadow: 0 0 0 2px #c30000;
border-color: #c30000;
}
.navds-form-field__error {
display: flex;
align-items: center;
@@ -58,7 +64,7 @@
margin: 0;
font-size: var(--a-font-size-xs);
font-weight: var(--a-font-weight-bold);
color: var(--a-icon-danger);
color: #c30000;
}
.navds-sr-only {
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 0.2.0
- Add ChevronDownIcon.
## 0.1.0
- Initial release with Information, Checkmark, Warning and XMark icons.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "0.1.0",
"version": "0.2.0",
"description": "SVG icons as React components for Aksel Mini",
"license": "MIT",
"type": "module",
@@ -0,0 +1,27 @@
import { forwardRef } from "react";
import type { SVGProps } from "react";
export const ChevronDownIcon = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
function ChevronDownIcon(props, ref) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
focusable={false}
aria-hidden
ref={ref}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.4 8.9 12 15.5l6.6-6.6-1.4-1.4-5.2 5.2-5.2-5.2-1.4 1.4Z"
fill="currentColor"
/>
</svg>
);
},
);
+1
View File
@@ -1,3 +1,4 @@
export { ChevronDownIcon } from "./ChevronDownIcon";
export { InformationIcon } from "./InformationIcon";
export { CheckmarkIcon } from "./CheckmarkIcon";
export { WarningIcon } from "./WarningIcon";
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.4 8.9 12 15.5l6.6-6.6-1.4-1.4-5.2 5.2-5.2-5.2-1.4 1.4Z" fill="#23262A"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 0.2.0
- Add Accordion and Modal components.
## 0.1.0
- Initial release with Button, BodyShort, Heading, TextField and Alert.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "0.1.0",
"version": "0.2.0",
"description": "React components for Aksel Mini",
"license": "MIT",
"type": "module",
@@ -0,0 +1,59 @@
import { forwardRef, useId, useState } from "react";
import type { HTMLAttributes, ReactNode } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { cn } from "../util/cn";
export interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
const AccordionRoot = forwardRef<HTMLDivElement, AccordionProps>(function Accordion(
{ className, children, ...rest },
ref,
) {
return (
<div ref={ref} className={cn("navds-accordion", className)} {...rest}>
{children}
</div>
);
});
export interface AccordionItemProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
/** Header title. */
title: ReactNode;
/** Start expanded without controlling state. */
defaultOpen?: boolean;
children: ReactNode;
}
export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
function AccordionItem({ title, defaultOpen = false, className, children, ...rest }, ref) {
const [open, setOpen] = useState(defaultOpen);
const contentId = useId();
return (
<div
ref={ref}
className={cn("navds-accordion__item", open && "navds-accordion__item--open", className)}
{...rest}
>
<button
type="button"
className="navds-accordion__header"
aria-expanded={open}
aria-controls={contentId}
onClick={() => setOpen((prev) => !prev)}
>
<ChevronDownIcon className="navds-accordion__chevron" />
<span className="navds-accordion__title">{title}</span>
</button>
<div id={contentId} className="navds-accordion__content" hidden={!open}>
{children}
</div>
</div>
);
},
);
export const Accordion = Object.assign(AccordionRoot, { Item: AccordionItem });
@@ -0,0 +1,2 @@
export { Accordion, AccordionItem } from "./Accordion";
export type { AccordionProps, AccordionItemProps } from "./Accordion";
+3 -3
View File
@@ -10,13 +10,13 @@ export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
size?: "xlarge" | "large" | "medium" | "small" | "xsmall";
/**
* Semantic heading level (h1–h6).
* @default 1
* @default "1"
*/
level?: 1 | 2 | 3 | 4 | 5 | 6;
level?: "1" | "2" | "3" | "4" | "5" | "6";
}
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(function Heading(
{ size = "medium", level = 1, className, ...rest },
{ size = "medium", level = "1", className, ...rest },
ref,
) {
return createElement(`h${level}`, {
+4
View File
@@ -8,3 +8,7 @@ export { TextField } from "./text-field";
export type { TextFieldProps } from "./text-field";
export { Alert } from "./alert";
export type { AlertProps } from "./alert";
export { Accordion, AccordionItem } from "./accordion";
export type { AccordionProps, AccordionItemProps } from "./accordion";
export { Modal } from "./modal";
export type { ModalProps } from "./modal";
+60
View File
@@ -0,0 +1,60 @@
import { forwardRef } from "react";
import type { HTMLAttributes, ReactNode } from "react";
import { createPortal } from "react-dom";
import { XMarkIcon } from "@navikt/aksel-icons";
import { cn } from "../util/cn";
export interface ModalProps extends HTMLAttributes<HTMLDivElement> {
/** Whether the modal is rendered. */
open: boolean;
/** Called when the user closes the modal via the close button or backdrop. */
onClose?: () => void;
/** Accessible label for the dialog. */
label?: string;
/**
* Close when clicking the backdrop.
* @default true
*/
closeOnBackdropClick?: boolean;
children: ReactNode;
}
export const Modal = forwardRef<HTMLDivElement, ModalProps>(function Modal(
{ open, onClose, label, closeOnBackdropClick = true, className, children, ...rest },
ref,
) {
if (!open) {
return null;
}
return createPortal(
<div
className="navds-modal__overlay"
onClick={(event) => {
if (closeOnBackdropClick && event.target === event.currentTarget) {
onClose?.();
}
}}
>
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-label={label}
className={cn("navds-modal", className)}
{...rest}
>
<button
type="button"
className="navds-modal__close"
aria-label="Lukk"
onClick={onClose}
>
<XMarkIcon />
</button>
{children}
</div>
</div>,
document.body,
);
});
+2
View File
@@ -0,0 +1,2 @@
export { Modal } from "./Modal";
export type { ModalProps } from "./Modal";
+1 -1
View File
@@ -1,4 +1,4 @@
type ClassValue = string | false | null | undefined;
type ClassValue = string | number | false | null | undefined;
export function cn(...classes: ClassValue[]): string {
return classes.filter(Boolean).join(" ");
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 0.2.0
- Color, spacing, radius and typography tokens generated as CSS custom properties.
## 0.1.0
- Initial release.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "0.1.0",
"version": "0.2.0",
"description": "Design tokens for Aksel Mini, emitted as CSS custom properties",
"license": "MIT",
"type": "module",
+1139
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -1,3 +1,6 @@
packages:
- "packages/**"
- "aksel.nav.no/**"
allowBuilds:
esbuild: true