diff --git a/apps/rhc-templates/src/app/collage/page.tsx b/apps/rhc-templates/src/app/collage/page.tsx
index 68e9269ff..f6755cdba 100644
--- a/apps/rhc-templates/src/app/collage/page.tsx
+++ b/apps/rhc-templates/src/app/collage/page.tsx
@@ -1,6 +1,7 @@
'use client';
-import { AccordionProvider, Link, Paragraph } from '@rijkshuisstijl-community/components-react';
+
+import { AccordionProvider, Blockquote, Link, Paragraph } from '@rijkshuisstijl-community/components-react';
import { Avatar, Pagination } from '@amsterdam/design-system-react';
import {
IconAlertTriangle,
@@ -13,7 +14,6 @@ import {
import {
Alert,
BadgeCounter,
- Blockquote,
BreadcrumbNav,
BreadcrumbNavLink,
Button,
@@ -112,9 +112,7 @@ export default function Collage() {
In het NL Design System verzamelen we principes, handvatten, elementen, patronen en richtlijnen.
-
-
Ik heb het nog nooit gedaan dus ik denk dat ik het wel kan
-
+ Ik heb het nog nooit gedaan dus ik denk dat ik het wel kan
diff --git a/apps/rhc-templates/src/app/page/page.tsx b/apps/rhc-templates/src/app/page/page.tsx
index ab61563a7..3b4d2fefb 100644
--- a/apps/rhc-templates/src/app/page/page.tsx
+++ b/apps/rhc-templates/src/app/page/page.tsx
@@ -1,7 +1,6 @@
'use client';
import {
- Blockquote,
Button,
ButtonLink,
Heading,
@@ -30,7 +29,7 @@ import {
Figure,
FigureCaption,
} from '@utrecht/component-library-react/dist/css-module';
-import { ActionGroup, Link, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
+import { ActionGroup, Blockquote, Link, Logo, Paragraph } from '@rijkshuisstijl-community/components-react';
import { HeadingGroup } from '@utrecht/component-library-react';
export default function Page() {
@@ -161,11 +160,9 @@ export default function Page() {
officia deserunt mollit anim id est laborum.
Dit is een normale link
-
-
- Lorem ipsum dolor sit amet, consectetur ad * isicing elit, sed do eiusmod *
-
-
+
+ Lorem ipsum dolor sit amet, consectetur ad * isicing elit, sed do eiusmod *
+
Dit is een H5
diff --git a/packages/components-css/blockquote/index.scss b/packages/components-css/blockquote/index.scss
new file mode 100644
index 000000000..67815f7c8
--- /dev/null
+++ b/packages/components-css/blockquote/index.scss
@@ -0,0 +1,15 @@
+@import "../node_modules/@utrecht/components/blockquote/src/";
+
+.utrecht-blockquote {
+ color: var(--utrecht-blockquote-content-color, inherit);
+ display: flex;
+ flex-direction: column;
+ font-family: var(--utrecht-blockquote-content-font-family, inherit);
+ font-size: var(--utrecht-blockquote-content-font-size, inherit);
+ row-gap: var(--utrecht-blockquote-row-gap);
+}
+
+.utrecht-blockquote__attribution {
+ color: var(--utrecht-blockquote-attribution-color, inherit);
+ font-family: var(--utrecht-blockquote-attribution-font-family, inherit);
+}
diff --git a/packages/components-css/index.scss b/packages/components-css/index.scss
index aeea1fce0..f11549745 100644
--- a/packages/components-css/index.scss
+++ b/packages/components-css/index.scss
@@ -4,6 +4,7 @@
*/
@import "accordion/index";
+@import "blockquote/index";
@import "alert/index";
@import "link/index";
@import "logo/index";
diff --git a/packages/components-react/src/Blockquote.test.tsx b/packages/components-react/src/Blockquote.test.tsx
new file mode 100644
index 000000000..f07b75d42
--- /dev/null
+++ b/packages/components-react/src/Blockquote.test.tsx
@@ -0,0 +1,125 @@
+import { render } from '@testing-library/react';
+import { createRef } from 'react';
+import { Blockquote } from './Blockquote';
+import '@testing-library/jest-dom';
+
+describe('Blockquote', () => {
+ it('renders an HTML blockquote element', () => {
+ const { container } = render( );
+
+ const blockquote = container.querySelector('blockquote:only-child');
+
+ expect(blockquote).toBeInTheDocument();
+ });
+
+ it('renders a block element', () => {
+ const { container } = render( );
+
+ const paragraph = container.querySelector(':only-child');
+
+ expect(paragraph).toHaveStyle({ display: 'block' });
+ });
+
+ it('renders a design system BEM class name: utrecht-blockquote', () => {
+ const { container } = render( );
+
+ const paragraph = container.querySelector(':only-child');
+
+ expect(paragraph).toHaveClass('utrecht-blockquote');
+ });
+
+ it('renders rich text content', () => {
+ const { container } = render(
+
+ Hello, world
+ ,
+ );
+
+ const blockquote = container.querySelector(':only-child');
+
+ const richText = blockquote?.querySelector('code');
+
+ expect(richText).toBeInTheDocument();
+ });
+
+ // Make sure Blockquote isn't implemented as `` element,
+ // because then it would be impossible to render `
` children.
+ it('can render multiple paragraph of rich text content', () => {
+ const { container } = render(
+
+ Hello...
+ ...world
+ ,
+ );
+
+ const blockquote = container.querySelector(':only-child');
+
+ const richText = blockquote?.querySelector('p + p');
+
+ expect(richText).toBeInTheDocument();
+ });
+
+ it('can be hidden', () => {
+ const { container } = render( );
+
+ const blockquote = container.querySelector(':only-child');
+
+ expect(blockquote).not.toBeVisible();
+ });
+
+ it('can have a custom class name', () => {
+ const { container } = render( );
+
+ const blockquote = container.querySelector(':only-child');
+
+ expect(blockquote).toHaveClass('callout');
+ });
+
+ it('can have a additional class name', () => {
+ const { container } = render( );
+
+ const blockquote = container.querySelector(':only-child');
+
+ expect(blockquote).toHaveClass('callout');
+ expect(blockquote).toHaveClass('utrecht-blockquote');
+ });
+
+ describe('attribution', () => {
+ it('can contain rich text', () => {
+ const { container } = render(
+ The C Programming Language}>
+ Hello, world
+ ,
+ );
+
+ const richText = container.querySelector('cite');
+
+ expect(richText).toBeInTheDocument();
+ });
+
+ it('follows the the content in document order', () => {
+ const { container } = render(
+ The C Programming Language}>
+ Hello, world
+ ,
+ );
+
+ const attribution = container.querySelector('cite');
+ const content = container.querySelector('p');
+
+ if (attribution && content) {
+ expect(attribution.compareDocumentPosition(content)).toBe(2);
+ }
+ });
+ });
+
+ it('supports ForwardRef in React', () => {
+ const ref = createRef();
+
+ const { container } = render( );
+
+ const blockquote = container.querySelector(':only-child');
+
+ expect(ref.current).toBe(blockquote);
+ });
+});
diff --git a/packages/components-react/src/Blockquote.tsx b/packages/components-react/src/Blockquote.tsx
new file mode 100644
index 000000000..c808ec94a
--- /dev/null
+++ b/packages/components-react/src/Blockquote.tsx
@@ -0,0 +1,21 @@
+import {
+ Blockquote as UtrechtBlockquote,
+ BlockquoteProps as UtrechtBlockquoteProps,
+} from '@utrecht/component-library-react';
+import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';
+import '@rijkshuisstijl-community/components-css/index.scss';
+
+export type { UtrechtBlockquoteProps as BlockquoteProps };
+
+export const Blockquote = forwardRef(
+ (
+ { children, attribution, ...restProps }: PropsWithChildren,
+ ref: ForwardedRef,
+ ) => (
+
+ {children}
+
+ ),
+);
+
+Blockquote.displayName = 'Blockquote';
diff --git a/packages/storybook/src/community/accordion.md b/packages/storybook/src/community/accordion.md
index aac3a530e..11c4d2b2c 100644
--- a/packages/storybook/src/community/accordion.md
+++ b/packages/storybook/src/community/accordion.md
@@ -1,3 +1,53 @@
# Rijkshuisstijl Community accordion component
+
+Je kunt een accordion gebruiken lange pagina's korter te maken zodat de gebruiker niet lang moet scrollen om de relevante informatie te vinden.
+
+De inhoud verbergen heeft als nadeel dat "zoeken in pagina" geen resultaten oplevert de inhoud die verborgen is. Als je weet op welke zoekterm de gebruiker heeft gezocht om op de pagina met accordion te komen, dan kun je de sections die de zoekterm bevatten automatisch _expanded_ maken.
+
+## Tekst
+
+Het is belangrijk duidelijke koppen te hebben, omdat de gebruiker niet de verborgen inhoud kan "scannen" om relevante informatie te vinden.
+
+## HTML
+
+Gebruik een `` element waarmee je de inhoud van de section kunt weergeven en verbergen. Gebruik altijd het `aria-expanded` attribuut zodat duidelijk wat het effect is van de button activeren. Gebruik `aria-expanded="true"` wanneer de inhoud beschikbaar is, gebruik `aria-expanded="false"` wanneer de inhoud verborgen is.
+
+Plaats de button in de heading, zodat gebruikers die via een overzicht van headings door de pagina navigeren de informatie makkelijk kunnen vinden.
+
+Plaats de inhoud van de _expandable region_ in een `` element, zodat gebruikers die via een overzicht van _landmarks_ door de pagina navigeren de informatie makkelijk kunnen vinden (het `section` element maakt een `region` landmark). Gebruik `aria-labelledby` om de `section` te koppelen aan de heading, omdat het is belangrijk dat de landmark een duidelijk label heeft. Zonder label is de _landmark navigation_ onduidelijk omdat er meerdere regions zijn zonder naam, waarvan niet duidelijk is wat de inhoud is.
+
+Zelfs de inhoud van een verborgen _expandable region_ moet in de HTML-code staan, wanneer je _server-side rendering_ gebruikt zodat zoekmachines dan toch toegang hebben tot de volledige inhoud. De inhoud van een verborgen _expandable region_ kan met CSS onzichtbaar gemaakt worden.
+
+Wanneer je accordion heel veel onderdelen heeft, dan kunnen gebruikers van _landmark navigation_ moeilijker andere landmarks in de pagina vinden. Om bij de `contentinfo` landmark te komen (de page footer), moet de gebruiker eerst alle accordion onderdelen overslaan. Gebruik voor een accordion met heel veel onderdelen een `` of `
` element in plaats van het `` element zodat het geen landmark wordt. Gebruikers kunnen de informatie dan nog wel vinden via _heading navigation_.
+
+### Zo moet het niet
+
+Verwijder het `aria-expanded` attribuut niet, want `aria-expanded="false"` is niet hetzelfde als geen `aria-expanded` attribuut hebben. "_Boolean attributes_" van ARIA werken niet hetzelfde als _boolean attributes_ van HTML. ([Boolean attributes in HTML and ARIA: what's the difference? — Hidde de Vries](https://hidde.blog/boolean-attributes-in-html-and-aria-whats-the-difference/)).
+
+Plaats niet de heading in de button in plaats van andersom, omdat de heading dan niet toegankelijk is voor hulpmiddelen die een overzicht maken van alle headings op een pagina.
+
+Wacht niet met het plaatsen van de inhoud van de _expandable region_ tot de gebruiker de button activeert, want zoekmachines gebruiken alleen de initiële inhoud van de pagina zonder op buttons te gebruiken.
+
+## Visueel ontwerp
+
+De icoon plaatsen vóór de heading is meest duidelijk. Als het icoon voor _expanded_ of _not expanded_ uitgelijnd is aan het eind van de regel, dan is het minder duidelijk dat de heading een button is die voor inklappen en uitklappen zorgt. Sommige gebruikers met een beperkt gezichtsveld kunnen het icoon bijvoorbeeld niet zien wanneer ze naar naar de heading kijken, als er grote afstand zit tussen de heading en het icoon.
+
+## Relevante WCAG eisen
+
+De WCAG eisen voor de button component en de heading component gelden ook voor de accordion header.
+
+Let extra op voor deze onderdelen:
+
+- [WCAG eis 1.3.1](https://www.w3.org/TR/WCAG21/#info-and-relationships): de _heading level_ van de accordion sections is afhankelijk van waar in de pagina de accordion is geplaatst, dit kan per pagina verschillen.
+- [WCAG eis 1.3.6](https://www.w3.org/TR/WCAG21/#identify-purpose): gebruik `aria-controls` voor de button, en gebruik een `region` landmark voor de _expandable region_ (gebruik daarvoor het HTML-element ``)
+- [WCAG eis 1.4.3](https://www.w3.org/TR/WCAG21/#contrast-minimum): contrast tussen tekst en achtergrond en tussen icoon en achtergrond is voldoende in alle varianten, alle interactieve statussen en alle mogelijke combinaties.
+- [WCAG eis 3.2.1](https://www.w3.org/TR/WCAG21/#on-focus): maak de accordion niet automatisch _expanded_ als de button focus krijgt.
+- [WCAG eis 2.1.3](https://www.w3.org/TR/WCAG21/#keyboard-no-exception): ondersteun ook de optionele toetsen: `Down Arrow`, `Up Arrow`, `Home` en `End`. `Space` moet de button activeren, niet de pagina scrollen (gebruik `keypressEvt.preventDefault()`).
+- [WCAG eis 2.4.6](https://www.w3.org/TR/WCAG21/#headings-and-labels): de tekst die zowel wordt gebruikt als heading en als label voor de button moet duidelijk zijn, omdat de inhoud niet altijd zichtbaar is.
+- [WCAG eis 2.4.10](https://www.w3.org/TR/WCAG21/#section-headings): accordions die onderdeel vormen van de lopende tekst moeten section headings gebruiken.
+
+## Relevante info
+
+- [Accordion (Sections With Show/Hide Functionality) - W3C ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
diff --git a/packages/storybook/src/community/blockquote.md b/packages/storybook/src/community/blockquote.md
new file mode 100644
index 000000000..178ac8e14
--- /dev/null
+++ b/packages/storybook/src/community/blockquote.md
@@ -0,0 +1,14 @@
+
+
+# Rijkshuisstijl Community blockquote component
+
+Quotes worden gebruikt om uitspraken te accentueren. De quote bestaat uit een uitspraak en een bronvermelding.
+
+## Terminologie
+
+- **blockquote**: van het [HTML element ``](https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element). MDN noemt het ["Block Quotation element"](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote), misschien zou dat nog wel beter zijn dan "Blockquote".
+- **attribution**: verwijzing naar de bron van het citaat. Experimentele naam.
+
+## Relevante WCAG eisen
+
+- [WCAG eis 3.1.2](https://www.w3.org/TR/WCAG21/#language-of-parts): wanneer het citaat in een andere taal is geschreven dan de pagina, dan moet het `lang` attribuut gebruikt worden om de taal duidelijk te maken.
diff --git a/packages/storybook/src/community/blockquote.stories.tsx b/packages/storybook/src/community/blockquote.stories.tsx
new file mode 100644
index 000000000..ca7df8972
--- /dev/null
+++ b/packages/storybook/src/community/blockquote.stories.tsx
@@ -0,0 +1,42 @@
+/* @license CC0-1.0 */
+
+import { Blockquote } from '@rijkshuisstijl-community/components-react';
+import { Meta, StoryObj } from '@storybook/react';
+import readme from './blockquote.md?raw';
+
+const meta = {
+ title: 'Rijkshuisstijl/Blockquote',
+ id: 'rijkshuisstijl-blockquote',
+ component: Blockquote,
+ argTypes: {
+ attribution: {
+ name: 'attribution',
+ type: { name: 'string', required: false },
+ },
+ },
+ args: {
+ children: 'Lorem ipsum dolor sit amet, consectetur ad * isicing elit, sed do eiusmod *',
+ },
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ description: {
+ component: readme,
+ },
+ },
+ },
+} as Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Attribution: Story = {
+ name: 'Attribution',
+ args: {
+ children: '"Ik heb het nog nooit gedaan dus ik denk dat ik het wel kan"',
+ attribution: '— Pippi Langkous',
+ },
+};
diff --git a/proprietary/design-tokens/figma/figma.tokens.json b/proprietary/design-tokens/figma/figma.tokens.json
index 06ca3cddc..e7c315457 100644
--- a/proprietary/design-tokens/figma/figma.tokens.json
+++ b/proprietary/design-tokens/figma/figma.tokens.json
@@ -1763,6 +1763,10 @@
"value": "{rhc.space.0}",
"type": "spacing"
},
+ "row-gap": {
+ "value": "{rhc.space.150}",
+ "type": "spacing"
+ },
"background-color": {
"value": "transparent",
"type": "color"
diff --git a/proprietary/design-tokens/token-transformer.mjs b/proprietary/design-tokens/token-transformer.mjs
index 5228ce63e..c0208d27c 100644
--- a/proprietary/design-tokens/token-transformer.mjs
+++ b/proprietary/design-tokens/token-transformer.mjs
@@ -13,7 +13,6 @@ const init = async ({ input, output }) => {
const excludes = [
'components/avatar',
'components/backdrop',
- 'components/blockquote',
'components/breadcrumb',
'components/button-group',
'components/checkbox',