diff --git a/packages/css/src/components/file-input/README.md b/packages/css/src/components/file-input/README.md
new file mode 100644
index 0000000000..4ab50f20f3
--- /dev/null
+++ b/packages/css/src/components/file-input/README.md
@@ -0,0 +1,9 @@
+
+
+# File Input
+
+Allows the user to upload one or more files from their device.
+
+## Visual considerations
+
+The filename label and button are displayed in the language of the browser and can vary between browsers and operating systems.
diff --git a/packages/css/src/components/file-input/file-input.scss b/packages/css/src/components/file-input/file-input.scss
new file mode 100644
index 0000000000..0a8942825b
--- /dev/null
+++ b/packages/css/src/components/file-input/file-input.scss
@@ -0,0 +1,68 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+@import "../../common/text-rendering";
+
+@mixin reset-button {
+ border: 0;
+ border-radius: 0; // Reset rounded borders on iOS devices
+ box-sizing: border-box;
+}
+
+.ams-file-input {
+ background-color: var(--ams-file-input-background-color);
+ border: var(--ams-file-input-border);
+ color: var(--ams-file-input-color);
+ cursor: var(--ams-file-input-cursor);
+ font-family: var(--ams-file-input-font-family);
+ font-size: var(--ams-file-input-font-size);
+ font-weight: var(--ams-file-input-font-weight);
+ line-height: var(--ams-file-input-line-height);
+ max-inline-size: calc(100% - var(--ams-file-input-padding-inline) * 2);
+ outline-offset: 0.25rem; // Double the default focus outline offset to compensate for the dashed border
+ padding-block: var(--ams-file-input-padding-block);
+ padding-inline: var(--ams-file-input-padding-inline);
+ touch-action: manipulation;
+
+ @include text-rendering;
+}
+
+.ams-file-input:disabled {
+ color: var(--ams-file-input-disabled-color);
+ cursor: var(--ams-file-input-disabled-cursor);
+}
+
+.ams-file-input::file-selector-button {
+ appearance: none; // Reset default appearance on iOS devices
+ background-color: var(--ams-file-input-file-selector-button-background-color);
+ box-shadow: var(--ams-file-input-file-selector-button-box-shadow);
+ color: var(--ams-file-input-file-selector-button-color);
+ cursor: var(--ams-file-input-file-selector-button-cursor);
+ font-family: inherit;
+ font-size: inherit; // iOS specific fix
+ font-weight: inherit;
+ margin-inline-end: var(--ams-file-input-file-selector-button-margin-inline-end);
+ padding-block: var(--ams-file-input-file-selector-button-padding-block);
+ padding-inline: var(--ams-file-input-file-selector-button-padding-inline);
+
+ @media screen and (-ms-high-contrast: active), screen and (forced-colors: active) {
+ border: var(
+ --ams-file-input-file-selector-button-forced-color-mode-border
+ ); // add border because forced colors changes box-shadow to none
+ }
+
+ @include reset-button;
+}
+
+.ams-file-input:disabled::file-selector-button {
+ box-shadow: var(--ams-file-input-file-selector-button-disabled-box-shadow);
+ color: var(--ams-file-input-disabled-color);
+ cursor: var(--ams-file-input-file-selector-button-disabled-cursor);
+}
+
+.ams-file-input:not(:disabled):hover::file-selector-button {
+ box-shadow: var(--ams-file-input-file-selector-button-hover-box-shadow);
+ color: var(--ams-file-input-file-selector-button-hover-color);
+}
diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss
index a3ac841829..5f7cf2d09a 100644
--- a/packages/css/src/components/index.scss
+++ b/packages/css/src/components/index.scss
@@ -4,6 +4,7 @@
*/
/* Append here */
+@import "./file-input/file-input";
@import "./field/field";
@import "./select/select";
@import "./time-input/time-input";
diff --git a/packages/react/src/FileInput/FileInput.test.tsx b/packages/react/src/FileInput/FileInput.test.tsx
new file mode 100644
index 0000000000..8875679be5
--- /dev/null
+++ b/packages/react/src/FileInput/FileInput.test.tsx
@@ -0,0 +1,37 @@
+import { render } from '@testing-library/react'
+import { createRef } from 'react'
+import { FileInput } from './FileInput'
+import '@testing-library/jest-dom'
+
+describe('File input', () => {
+ it('renders', () => {
+ const { container } = render()
+ const component = container.querySelector('input[type="file"]')
+
+ expect(component).toBeInTheDocument()
+ expect(component).toBeVisible()
+ })
+
+ it('renders a design system BEM class name', () => {
+ const { container } = render()
+ const component = container.querySelector('input[type="file"]')
+
+ expect(component).toHaveClass('ams-file-input')
+ })
+
+ it('renders an additional class name', () => {
+ const { container } = render()
+ const component = container.querySelector('input[type="file"]')
+
+ expect(component).toHaveClass('ams-file-input extra')
+ })
+
+ it('supports ForwardRef in React', () => {
+ const ref = createRef()
+
+ const { container } = render()
+ const component = container.querySelector('input[type="file"]')
+
+ expect(ref.current).toBe(component)
+ })
+})
diff --git a/packages/react/src/FileInput/FileInput.tsx b/packages/react/src/FileInput/FileInput.tsx
new file mode 100644
index 0000000000..3df5f8cec0
--- /dev/null
+++ b/packages/react/src/FileInput/FileInput.tsx
@@ -0,0 +1,18 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+import clsx from 'clsx'
+import { forwardRef } from 'react'
+import type { ForwardedRef, InputHTMLAttributes } from 'react'
+
+export type FileInputProps = InputHTMLAttributes
+
+export const FileInput = forwardRef(
+ ({ className, ...restProps }: FileInputProps, ref: ForwardedRef) => (
+
+ ),
+)
+
+FileInput.displayName = 'FileInput'
diff --git a/packages/react/src/FileInput/README.md b/packages/react/src/FileInput/README.md
new file mode 100644
index 0000000000..5fe4f7d710
--- /dev/null
+++ b/packages/react/src/FileInput/README.md
@@ -0,0 +1,5 @@
+
+
+# React File Input component
+
+[File Input documentation](../../../css/src/components/file-input/README.md)
diff --git a/packages/react/src/FileInput/index.ts b/packages/react/src/FileInput/index.ts
new file mode 100644
index 0000000000..73c0b3a40c
--- /dev/null
+++ b/packages/react/src/FileInput/index.ts
@@ -0,0 +1,2 @@
+export { FileInput } from './FileInput'
+export type { FileInputProps } from './FileInput'
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 9c8a57cd88..4334528431 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -4,6 +4,7 @@
*/
/* Append here */
+export * from './FileInput'
export * from './Field'
export * from './Select'
export * from './TimeInput'
diff --git a/proprietary/tokens/src/components/ams/file-input.tokens.json b/proprietary/tokens/src/components/ams/file-input.tokens.json
new file mode 100644
index 0000000000..b0f31f9437
--- /dev/null
+++ b/proprietary/tokens/src/components/ams/file-input.tokens.json
@@ -0,0 +1,42 @@
+{
+ "ams": {
+ "file-input": {
+ "background-color": { "value": "{ams.color.primary-white}" },
+ "border": { "value": "{ams.border.width.sm} dashed {ams.color.neutral-grey3}" },
+ "color": { "value": "{ams.color.primary-black}" },
+ "cursor": { "value": "{ams.action.activate.cursor}" },
+ "font-family": { "value": "{ams.text.font-family}" },
+ "font-size": { "value": "{ams.text.level.5.font-size}" },
+ "font-weight": { "value": "{ams.text.font-weight.normal}" },
+ "line-height": { "value": "{ams.text.level.5.line-height}" },
+ "outline-offset": { "value": "{ams.focus.outline-offset}" },
+ "padding-block": { "value": "{ams.space.inside.md}" },
+ "padding-inline": { "value": "{ams.space.inside.md}" },
+ "disabled": {
+ "color": { "value": "{ams.color.neutral-grey2}" },
+ "cursor": { "value": "{ams.action.disabled.cursor}" }
+ },
+ "file-selector-button": {
+ "background-color": { "value": "{ams.color.primary-white}" },
+ "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-blue}" },
+ "color": { "value": "{ams.color.primary-blue}" },
+ "cursor": { "value": "{ams.action.activate.cursor}" },
+ "margin-inline-end": { "value": "{ams.space.inside.md}" },
+ "padding-block": { "value": "{ams.space.inside.xs}" },
+ "padding-inline": { "value": "{ams.space.inside.md}" },
+ "hover": {
+ "box-shadow": { "value": "inset 0 0 0 {ams.border.width.lg} {ams.color.dark-blue}" },
+ "color": { "value": "{ams.color.dark-blue}" }
+ },
+ "disabled": {
+ "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.neutral-grey2}" },
+ "color": { "value": "{ams.color.neutral-grey2}" },
+ "cursor": { "value": "{ams.action.disabled.cursor}" }
+ },
+ "forced-color-mode": {
+ "border": { "value": "{ams.border.width.md} solid" }
+ }
+ }
+ }
+ }
+}
diff --git a/storybook/src/components/FileInput/FileInput.docs.mdx b/storybook/src/components/FileInput/FileInput.docs.mdx
new file mode 100644
index 0000000000..010919d592
--- /dev/null
+++ b/storybook/src/components/FileInput/FileInput.docs.mdx
@@ -0,0 +1,29 @@
+import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
+import * as FileInputStories from "./FileInput.stories.tsx";
+import README from "../../../../packages/css/src/components/file-input/README.md?raw";
+
+
+
+{README}
+
+
+
+
+
+## Multiple Files
+
+Allow multiple files to be selected. The label will update to show the number of files selected.
+
+
+
+## Accept
+
+Limit the types of files that can be selected. Some examples are `image/*`, `video/*`, or `audio/*`. To limit to a specific file type, use the MIME type, such as `application/pdf`.
+
+- [MDN File Input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#limiting_accepted_file_types): More examples
+
+
+
+## Disabled
+
+
diff --git a/storybook/src/components/FileInput/FileInput.stories.tsx b/storybook/src/components/FileInput/FileInput.stories.tsx
new file mode 100644
index 0000000000..19758a651d
--- /dev/null
+++ b/storybook/src/components/FileInput/FileInput.stories.tsx
@@ -0,0 +1,52 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+import { FileInput } from '@amsterdam/design-system-react/src'
+import { Meta, StoryObj } from '@storybook/react'
+
+const meta = {
+ title: 'Components/Forms/File Input',
+ component: FileInput,
+ args: {
+ accept: undefined,
+ multiple: false,
+ disabled: false,
+ },
+ argTypes: {
+ accept: {
+ control: {
+ type: 'text',
+ },
+ },
+ multiple: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const Multiple: Story = {
+ args: { multiple: true },
+}
+
+export const Accept: Story = {
+ args: { accept: 'application/pdf' },
+}
+
+export const Disabled: Story = {
+ args: { disabled: true },
+}