7 Commits
Author SHA1 Message Date
Aksel Mini 834b471d80 🔖 Release 0.3.0
- @navikt/ds-react: new Link component with action and neutral
  variants and an inlineText mode.
- @navikt/ds-css: Link styles.
- @navikt/ds-tokens and @navikt/aksel-icons: version alignment.
2026-09-12 09:00:00 +02:00
Aksel Mini 39f49989eb ✨ Add Link component
Inline link with action (default) and neutral variants, plus an
inlineText mode that keeps the underline when embedded in body text.
Includes styles and a website example.
2026-09-10 13:00:00 +02:00
Aksel Mini 7b7b13f6e6 📝 Backfill 0.1.0 changelog entries
Fill in proper release notes for v0.1.0 in every package. The ds-css
entry incorrectly listed Accordion and Modal, which shipped in 0.2.0.
2026-09-03 09:20: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
34 changed files with 1778 additions and 10 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"
}
}
+123
View File
@@ -0,0 +1,123 @@
import { useState } from "react";
import {
Accordion,
Alert,
BodyShort,
Button,
Heading,
Link,
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">
Link
</Heading>
<div className="example__preview example__preview--column">
<BodyShort>
Les mer om <Link href="#top" inlineText>hvordan vi behandler data</Link> før du
søker, eller gå tilbake til <Link href="#top" variant="neutral">forsiden</Link>.
</BodyShort>
</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,
},
});
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## 0.3.0
- :sparkles: Add Link styles, including the inlineText underline variant.
## 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 (primary, secondary,
tertiary and danger variants), BodyShort, Heading and TextField,
all built on CSS custom properties from @navikt/ds-tokens.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "0.1.0",
"version": "0.3.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,
+3
View File
@@ -4,3 +4,6 @@
@import "./button.css";
@import "./textfield.css";
@import "./alert.css";
@import "./accordion.css";
@import "./modal.css";
@import "./link.css";
+26
View File
@@ -0,0 +1,26 @@
/**
* Link
*/
.navds-link {
color: var(--a-icon-action);
text-decoration: none;
text-underline-offset: 0.15em;
}
.navds-link:hover {
text-decoration: underline;
}
.navds-link--neutral {
color: var(--a-text-default);
}
.navds-link--inline-text {
text-decoration: underline;
}
.navds-link:focus-visible {
outline: 2px solid var(--a-border-focus);
outline-offset: 2px;
}
+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;
}
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## 0.3.0
- Maintenance release, version alignment with the other packages.
## 0.2.0
- Add ChevronDownIcon.
## 0.1.0
- Initial release with hand-written React icon wrappers for
Information, Checkmark, Warning and XMark, with the source SVGs
kept under `svg/` for reference.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "0.1.0",
"version": "0.3.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

+16
View File
@@ -0,0 +1,16 @@
# Changelog
## 0.3.0
- :sparkles: Add Link component with action and neutral variants and an
inlineText mode for links embedded in body text.
## 0.2.0
- Add Accordion and Modal components.
## 0.1.0
- Initial release with Button (primary, secondary, tertiary and
danger variants), BodyShort, Heading, TextField and Alert
components.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "0.1.0",
"version": "0.3.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}`, {
+6
View File
@@ -8,3 +8,9 @@ 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";
export { Link } from "./link";
export type { LinkProps } from "./link";
+34
View File
@@ -0,0 +1,34 @@
import { forwardRef } from "react";
import type { AnchorHTMLAttributes } from "react";
import { cn } from "../util/cn";
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/**
* Link variant.
* @default "action"
*/
variant?: "action" | "neutral";
/**
* Underlines the link so it reads as part of surrounding body text.
* @default false
*/
inlineText?: boolean;
}
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
{ variant = "action", inlineText = false, className, ...rest },
ref,
) {
return (
<a
ref={ref}
className={cn(
"navds-link",
`navds-link--${variant}`,
inlineText && "navds-link--inline-text",
className,
)}
{...rest}
/>
);
});
+2
View File
@@ -0,0 +1,2 @@
export { Link } from "./Link";
export type { LinkProps } from "./Link";
+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(" ");
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## 0.3.0
- Maintenance release, version alignment with the other packages.
## 0.2.0
- Color, spacing, radius and typography tokens generated as CSS custom properties.
## 0.1.0
- Initial release. Color, spacing, radius and typography tokens are
defined in `src/tokens.json` and compiled to CSS custom properties
on the `--a-*` namespace.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "0.1.0",
"version": "0.3.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