Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e514100
wip FileUpload
oddvernes Feb 24, 2026
e6c2009
format
oddvernes Feb 24, 2026
fa60cdc
hover & variant
oddvernes Feb 24, 2026
c14b4c1
variant story
oddvernes Feb 24, 2026
64a26c7
link variant
oddvernes Feb 24, 2026
50e66a8
test with Field
oddvernes Feb 25, 2026
000c0a7
keyboard focusable
oddvernes Feb 25, 2026
0f227cc
rest should go to input
oddvernes Feb 25, 2026
52fb1e7
change the prop-delegation cabal
oddvernes Feb 25, 2026
aed55db
adding some state variants for development
oddvernes Feb 26, 2026
29ab539
suggested readonly style
oddvernes Feb 26, 2026
7cd6b6c
wip working example
oddvernes Feb 26, 2026
9fc9e41
functional example for testing
oddvernes Feb 27, 2026
9be2624
wider
oddvernes Feb 27, 2026
0dc9990
Merge branch 'main' into feat/fileupload
oddvernes Mar 4, 2026
db9bdb8
make input a subcomponent instead
oddvernes Mar 4, 2026
a4bec45
export FileUpload
oddvernes Mar 4, 2026
370348d
add react-dropzone as dev-dep for testing
oddvernes Mar 4, 2026
6c660bb
add react-dropzone example
oddvernes Mar 4, 2026
a49ccf2
biome
oddvernes Mar 4, 2026
db73ffb
add aria-invalid to drop-zone story
oddvernes Mar 4, 2026
8e62c14
add component variables
oddvernes Mar 4, 2026
64b8201
width
oddvernes Mar 6, 2026
6da1c27
Merge branch 'main' into feat/fileupload
oddvernes Mar 6, 2026
52db577
Merge branch 'main' into feat/fileupload
oddvernes Mar 18, 2026
cb27b35
Merge branch 'main' into feat/fileupload
oddvernes Mar 19, 2026
8e18754
Merge branch 'main' into feat/fileupload
oddvernes Mar 24, 2026
1ccb133
use overlaid input instead of label
oddvernes Mar 24, 2026
f83b66f
try with build in Field like TextField
oddvernes Mar 24, 2026
657cd61
Merge branch 'main' into feat/fileupload
oddvernes Mar 25, 2026
13a9dcd
default icon
oddvernes Mar 25, 2026
f1516e7
put field outside
oddvernes Mar 26, 2026
5bd24c7
comments
oddvernes Mar 27, 2026
e00ada6
missing export
oddvernes Mar 27, 2026
1154aaf
temp example on www for sr testing
oddvernes Mar 27, 2026
568532a
scaffold docs pages
oddvernes Mar 27, 2026
1392825
fix: simplify css, remove label as concept since we now use descripti…
eirikbacker Apr 14, 2026
52beaef
Merge branch 'main' into feat/fileupload
oddvernes Apr 16, 2026
2da494f
lockfile
oddvernes Apr 16, 2026
1154b81
changeset
oddvernes Apr 17, 2026
09028fe
remove undefined export
oddvernes Apr 17, 2026
b2b3ac9
fix type error
oddvernes Apr 17, 2026
f2b66dd
biome
oddvernes Apr 17, 2026
3379086
comment out disabled story
oddvernes Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clear-clubs-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@digdir/designsystemet-react": minor
"@digdir/designsystemet-css": minor
---

**New component**: FileUpload
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Alert data-color="info">
Nothing here yet
</Alert>
3 changes: 3 additions & 0 deletions apps/www/app/content/components/file-upload/en/code.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Alert data-color="info">
Nothing here yet
</Alert>
3 changes: 3 additions & 0 deletions apps/www/app/content/components/file-upload/en/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Alert data-color="info">
Nothing here yet
</Alert>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Field, FileUpload, Label } from '@digdir/designsystemet-react';

export const FileUploadExample = () => (
<Field>
<Label>Upload profile picture</Label>
<Field.Description>description text</Field.Description>
<FileUpload>
<FileUpload.Description>Drop file here</FileUpload.Description>
<FileUpload.Description>
File must be in csv format and less than 2MB
</FileUpload.Description>
<FileUpload.Button>Upload file</FileUpload.Button>
<FileUpload.Input />
</FileUpload>
</Field>
);

export const FileUploadReactDropzone = () => (
<Field tabIndex={0}>
<Label>Upload profile picture</Label>
<Field.Description>description text</Field.Description>
<FileUpload>
<FileUpload.Description>Drop file here</FileUpload.Description>
<FileUpload.Description>
File must be in csv format and less than 2MB
</FileUpload.Description>
<FileUpload.Button>Upload file</FileUpload.Button>
<FileUpload.Input tabIndex={-1} />
</FileUpload>
</Field>
);
12 changes: 12 additions & 0 deletions apps/www/app/content/components/file-upload/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"no": {
"title": "FileUpload",
"subtitle": "`FileUpload` er et skjemaelement for å laste opp filer."
},
"en": {
"title": "FileUpload",
"subtitle": "`FileUpload` is a form element used to upload files."
},
"image": "Input.svg",
"cssFile": "file-upload.css"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Alert data-color="info">
Nothing here yet
</Alert>
7 changes: 7 additions & 0 deletions apps/www/app/content/components/file-upload/no/code.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Midlertidig fileUpload eksempel
regular
<Story story="FileUploadExample" />
simulated react dropzone (tabindex="-1" on input, tabindex="0" on Field)
<Story story="FileUploadReactDropzone" />

<ReactComponentDocs />
3 changes: 3 additions & 0 deletions apps/www/app/content/components/file-upload/no/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Alert data-color="info">
Nothing here yet
</Alert>
91 changes: 91 additions & 0 deletions packages/css/src/file-upload.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*double class to increase specificity above .ds-field :is() (0.2.0 + order)*/
.ds-file-upload {
--dsc-file-upload-background: var(--ds-color-surface-default);
--dsc-file-upload-background--hover: var(--ds-color-surface-tinted);
--dsc-file-upload-icon-color: var(--ds-color-text-subtle);
--dsc-file-upload-border-color: var(--ds-color-border-default);
--dsc-file-upload-border-radius: var(--ds-border-radius-md);
--dsc-file-upload-border-width: max(2px, 0.125rem);
--dsc-file-upload-border-style: dashed;
--dsc-file-upload-border-style--hover: solid;
--dsc-file-upload-color: currentcolor;
--dsc-file-upload-padding: var(--ds-size-8) var(--ds-size-6);
--dsc-file-upload-icon-size: var(--ds-size-8);
--dsc-file-upload-icon-url: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M11 2.25A5.75 5.75 0 0 0 5.25 8c0 .052-.017.1-.074.157a.66.66 0 0 1-.325.158A3.25 3.25 0 0 0 5.5 14.75a.75.75 0 0 0 0-1.5 1.75 1.75 0 0 1-.35-3.465c.745-.151 1.6-.747 1.6-1.785a4.25 4.25 0 0 1 8.076-1.854c.288.593.896 1.104 1.68 1.104h.744a3 3 0 0 1 1 5.83.75.75 0 1 0 .5 1.414 4.501 4.501 0 0 0-1.5-8.744h-.745c-.09 0-.236-.066-.33-.26A5.746 5.746 0 0 0 11 2.25m.47 8.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 1 1-1.06 1.06l-2.22-2.22v7.69a.75.75 0 0 1-1.5 0v-7.69l-2.22 2.22a.75.75 0 0 1-1.06-1.06z" clip-rule="evenodd"/></svg>');

position: relative;
border: var(--dsc-file-upload-border-width) var(--dsc-file-upload-border-style) var(--dsc-file-upload-border-color);
border-radius: var(--dsc-file-upload-border-radius);
padding: var(--dsc-file-upload-padding);
background: var(--dsc-file-upload-background);
user-select: none;
outline: none; /* Hide outline when react-dropzone adds tabindex="0" */
text-align: center;
@composes ds-print-preserve from './base.css';

/* React-dropzone puts tabindex="0" on whatever gets {...getRootProps()} and set tabindex="-1" on input.. */
.ds-field[tabindex]:focus-visible:has(&) {
outline: none;
}
.ds-field[tabindex]:focus-visible &,
&:focus-within {
@composes ds-focus--visible from './base.css';
}

&:hover,
&:has([readonly]) {
border-style: var(--dsc-file-upload-border-style--hover);
background-color: var(--dsc-file-upload-background--hover);
cursor: pointer;
& > .ds-button {
/*link hover to button*/
--dsc-button-background: var(--dsc-button-background--hover);
--dsc-button-color: var(--dsc-button-color--hover);
}
}
&:has([readonly]) {
cursor: not-allowed;
}
&:has([aria-invalid='true']) {
border-color: var(--ds-color-danger-border-default);
}

& > input[type='file'] {
position: absolute;
inset: 0;
margin: 0;
z-index: 1;
opacity: 0;
cursor: inherit;
}

/* Icon */
& > svg,
&:not(:has(> svg))::before {
color: var(--dsc-file-upload-icon-color);
display: block;
height: var(--dsc-file-upload-icon-size);
margin: 0 auto var(--ds-size-2);
width: var(--dsc-file-upload-icon-size);

@media (forced-colors: active) {
color: CanvasText;
}
}
&:not(:has(> svg))::before {
content: '';
background: currentColor;
mask: var(--dsc-file-upload-icon-url) center / contain no-repeat;
@media (forced-colors: active) {
background: CanvasText;
}
}
/* First line of text should be currentcolor */
& > [data-field='description']:not([data-field='description'] + [data-field='description']) {
color: var(--dsc-file-upload-color);
}
& > .ds-button {
width: fit-content;
margin: var(--ds-size-4) auto 0;
}
}
1 change: 1 addition & 0 deletions packages/css/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@import url('./input.css') layer(ds.components);
@import url('./field.css') layer(ds.components);
@import url('./fieldset.css') layer(ds.components);
@import url('./file-upload.css') layer(ds.components);
@import url('./alert.css') layer(ds.components);
@import url('./popover.css') layer(ds.components);
@import url('./skip-link.css') layer(ds.components);
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@testing-library/user-event": "14.6.1",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"react-dropzone": "15.0.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"rimraf": "6.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { forwardRef, type HTMLAttributes } from 'react';
import type { ButtonProps } from '../button/button';
import { Button } from '../button/button';

export type FileUploadButtonProps = Pick<ButtonProps, 'variant'> &
HTMLAttributes<HTMLSpanElement>;

export const FileUploadButton = forwardRef<
HTMLSpanElement,
FileUploadButtonProps
>(function FileUploadButton({ variant = 'secondary', ...rest }, ref) {
return (
<Button variant={variant} asChild>
<span ref={ref} {...rest} />
</Button>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { forwardRef, type HTMLAttributes } from 'react';
import { Paragraph, type ParagraphProps } from '../paragraph/paragraph';

export type FileUploadDescriptionProps = ParagraphProps &
HTMLAttributes<HTMLParagraphElement>;

export const FileUploadDescription = forwardRef<
HTMLParagraphElement,
FileUploadDescriptionProps
>(function FileUploadDescription(rest, ref) {
return <Paragraph data-field='description' ref={ref} {...rest} />;
});
17 changes: 17 additions & 0 deletions packages/react/src/components/file-upload/file-upload-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { forwardRef, type InputHTMLAttributes } from 'react';

export type FileUploadInputProps = InputHTMLAttributes<HTMLInputElement>;

export const FileUploadInput = forwardRef<
HTMLInputElement,
FileUploadInputProps
>(function FileUploadInput(rest, ref) {
return (
<input
title='' // Hide native "No file choosen" tooltip on Mac
type='file'
ref={ref}
{...rest}
/>
);
});
28 changes: 28 additions & 0 deletions packages/react/src/components/file-upload/file-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Meta, Canvas, Controls, Primary } from '@storybook/addon-docs/blocks';
import * as FileStories from './file-upload.stories';

<Meta of={FileStories} />

**Dokumentasjonen har blitt flyttet til [Designsystemet.no](https://designsystemet.no/no/components).**

<Primary />
<Controls />

### working example for testing
Issue: the button in the input gets focus and the screen reader reads "Label -> Ingen fil valgt -> Description inside dropzone wrapper" (then label and ingen fil valgt again but could be due to iframe)
<Canvas of={FileStories.WorkingExample} />

### Using react-dropzone
Issue: `react-dropzone` puts `tabindex="0"` on whatever gets `{...getRootProps()}` and set `tabindex="-1"` on input, so screen reader does not read label/description outside of the dropzone wrapper.
<Canvas of={FileStories.ReactDropZoneExample} />

<Canvas of={FileStories.Variants} />

### Link variant
<Canvas of={FileStories.LinkAlt} />

### readOnly test
<Canvas of={FileStories.ReadOnly} />

### Disabled test
<Canvas of={FileStories.Disabled} />
Loading
Loading