A11y: färger och kontrastratio

Påbörjad artikel. Byggs ut kontinuerligt.

Grundkrav

WCAG 2.0 nivå AA kräver kontrastratio på minst 4.5:1 för normal text och 3:1 för stor text. Stor text räknas som minst 18.66px (fetad) eller 24px (normal) och större. WCAG skriver även:

Tillfälligt:
Text eller bilder av text som ingår i en inaktiv användargränssnittskomponent, som är ren dekoration, som inte är synliga för någon, eller som ingår i en bild som innehåller väsentligt annat visuellt innehåll, har inget kontrastkrav.

Logotyper:
Text som är en del av en logotyp eller varumärke har inget minimikrav på kontrast.

Förbättrade kontraststandarder

1.4.6 Kontrast (förbättrad): Den visuella presentationen av text och bilder av text har ett kontrastförhållande på minst 7:1, förutom följande: (Nivå AAA)

Stor text:
Storskalig text och bilder av storskalig text har ett kontrastförhållande på minst 4,5:1;

Tillfälligt:
Text eller bilder av text som ingår i en inaktiv användargränssnittskomponent, som är ren dekoration, som inte är synliga för någon, eller som ingår i en bild som innehåller väsentligt annat visuellt innehåll, har inget kontrastkrav.

Logotyper:
Text som är en del av en logotyp eller varumärke har inget minimikrav på kontrast.

Klassiska WebAIMs contrast checker använder jag ofta och det finns många fler, men numera är det även inbyggt i dev console.

Grundläggande koncept

Nyans (hue) — Nyans hänvisar till färgen på själva bilden. Nyans är en grad på färghjulet (från 0 till 360 grader) — 0 (eller 360) är röd, 120 är grön, 240 är blå. Det definieras formellt som ”i vilken grad en stimulans kan beskrivas som lik eller olik stimuli som beskrivs som röd, grön, blå och gul” (källa).

Mättnad (saturation) — Färgmättnad hänvisar till färgintensiteten i en bild. När färgen är helt mättad anses färgen vara dess renaste (äkta) version. Mättnad är ett procentuellt värde: 0 % betyder en grå nyans och 100 % är helfärgen. Rena färger är helt mättade.

Luminans (luminance) — intensiteten av ljus som emitteras från en yta per ytenhet i en given riktning. Detta är mätningen av ljusstyrka, med en skala från vitt till svart. Luma (%) är intensiteten hos den akromatiska signalen som bidrar till vår färguppfattning. Ett mättnadsvärde på 0 indikerar mestadels grått medan 100 % ljusstyrka (eller L = 255) är vitt.

Intensitet (intensity) — Intensitet avser renheten hos en nyans. Den högsta intensiteten eller renheten hos en nyans är nyansen som den visas i spektrumet eller på färghjulet. En nyans som är reducerad i intensitet kallas en ton.

Nyans (tint) — En nyans är ett blandningsresultat av en originalfärg där vitt har lagts till. En nyans är ljusare än den ursprungliga färgen.

Nyans (shade) — En nyans är ett blandningsresultat av en originalfärg där svart har lagts till. En nyans är mörkare än den ursprungliga färgen.

Ton (tone) — Ton är ett resultat av att blanda en ren färg med valfri neutral/gråskala färg, inklusive de två ytterligheterna vit och svart. En ton skapas antingen genom blandning av en färg med grått, eller genom både toning och skuggning.

Chroma — Kvaliteten på en färgs renhet, intensitet eller mättnad. Med andra ord är chroma den upplevda styrkan hos en ytfärg, graden av visuell skillnad från en neutral grå med samma ljushet.

Ljusstyrka (Brightness/Lightness) — Ljusstyrka som i brightness är den relativa ljusheten eller mörkheten för en viss färg, från svart (ingen ljusstyrka) till vitt (full ljusstyrka). Ljuststyrka som i lightness är en procentsats; 0% är svart, 100% är vit.

Färgkontrast (Color Contrast) — Skillnaden i luminans mellan två närliggande färger eller överlagrade färger (förgrund/bakgrund).

Dynamiskt omfång (Dynamic Range) — Förhållandet mellan de största och minsta värdena som en viss kvantitet kan anta. För färgkontrast är detta skillnaden mellan den ljusaste luminansen och den mörkaste luminansen. Det tar tid för ögonen att anpassa sig till olika ljusnivåer, så designers måste ta hänsyn till det dynamiska omfånget för det mänskliga ögat som tillämpas på digitala skärmar.

Optisk bländning (Optical Glare) — Bländning orsakas av ett betydande förhållande mellan luminansen mellan objekt och bländningskällan. Detta kan även gälla digitala och tryckta källor, varvid reflekterat ljusare ljus gör det svårare för det mänskliga ögat att urskilja närliggande föremål. Du kan också tillämpa detta koncept på skärmbländning, som vad som händer när du använder en bärbar dator utomhus.

Färgkontrastförhållande (Color Contrast Ratio) — En egenskap hos ett digitalt displaysystem definierat som förhållandet mellan luminansen för den ljusaste färgen (vit) och den för den mörkaste färgen (svart) som systemet kan producera. Ett högt kontrastförhållande är en önskad aspekt av alla skärmar.

Human Eye Dynamic Range — Det dynamiska omfånget för det mänskliga ögat är cirka 20 stopp, eller 1 000 000:1. Kontrastkänsligheten hos det mänskliga ögat är störst när detaljfrekvensen i en scen är cirka 4 cykler per grad (källa).

Beräkna kontrastratio

Kontrastförhållanden kan variera från 1 till 21 (vanligtvis skrivet 1:1 till 21:1).

Contrast ratio = (L1 + 0,05) / (L2 + 0,05), varvid:

kirilloid har översatt luminansberäkningen till:

function luminance(r, g, b) {
    var a = [r, g, b].map(function (v) {
        v /= 255;
        return v <= 0.03928
            ? v / 12.92
            : Math.pow( (v + 0.055) / 1.055, 2.4 );
    });
    return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}

Man kan också beräkna med PHP. (även https://dev.to/alvaromontoro/building-your-own-color-contrast-checker-4j7o)

https://medium.muz.li/the-ultimate-ux-guide-to-color-design-4d0a18a706ed

Lägg till klass och attribut på heading för att t.ex. göra en horisontell linje efter en rubrik

I JS nedan skapar vi en checkruta på headingblocken som i sin tur lägger på en klass på elementet om ibockad. Längst ner hittar vi ipmAddExtraProps där vi lägger till den inmatade texten som data-attribut på elementet. På så vis kan vi använda samma text i en ::before-deklaration. Väldigt omständigt men det är ett sätt att göra det på om man inte kan injecta <span> och grejer runt texten.

import assign from 'lodash.assign';
import { appendClassNames } from './helpers/appendClassNames';

const { createHigherOrderComponent } = wp.compose;
const { Fragment } = wp.element;
const { InspectorControls } = wp.blockEditor;
const { PanelBody, SelectControl, CheckboxControl } = wp.components;
const { addFilter } = wp.hooks;
const { __, _x } = wp.i18n;


/**
 * Add spacing control attribute to block.
 *
 * @param {object} settings Current block settings.
 * @param {string} name Name of block.
 *
 * @returns {object} Modified block settings.
 */
const addControlAttributes = (settings, name) => {

	// Do nothing if it's another block than our defined ones.
	if (name !== 'core/heading') {
		return settings;
	}

	// Use Lodash's assign to gracefully handle if attributes are undefined
	settings.attributes = assign(settings.attributes, {
		headingline: {
			type: 'boolean',
			default: false
		}
	});

	return settings;
};



/**
 * Create HOC to add spacing control to inspector controls of block.
 */
const additionalSettings = createHigherOrderComponent((BlockEdit) => {
	return (props) => {

		// Do nothing if it's another block than our defined ones.
		if (props.name !== 'core/heading') {
			return (
				<BlockEdit {...props} />
			);
		}

		const { headingline } = props.attributes;

		return (
			<Fragment>
				<BlockEdit {...props} />
				<InspectorControls>
					<PanelBody
						title={_x('Heading settings','heading line label', 'ipm-gef')}
						initialOpen={false}
					>
						<CheckboxControl
							label={_x('Line', 'heading line label', 'ipm-gef')}
							checked={ headingline }
							onChange={(selectedLineOption) => {
								props.attributes.className = appendClassNames(props.attributes.className, 'has-heading-line', headingline, selectedLineOption)
								props.setAttributes({
									headingline: selectedLineOption,
								});
							}}
						/>

					</PanelBody>
				</InspectorControls>
			</Fragment>
		);
	};
}, 'additionalSettings');

addFilter('blocks.registerBlockType', 'ipm/settings/heading-attributes', addControlAttributes);
addFilter('editor.BlockEdit', 'ipm/settings/heading-controls', additionalSettings);



function ipmAddExtraProps(extraProps, blockType, attributes) {
	if (blockType.name === 'core/heading' && attributes.headingline === true) {

		return {
			...extraProps,
			'data-heading-content': attributes.content,
		};
	}

	return extraProps;
}
addFilter('blocks.getSaveContent.extraProps', 'ipm/add-extra-props', ipmAddExtraProps);
&.has-heading-line {
		background-color: var(--color-white);
		padding-right: 1rem;
		position: relative;
		display: flex;

		&::before {
			background: var(--color-gray--darkest);
			content: "";
			display: inline-block;
			width: calc(100%);
			height: 1px;
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
		}

		&::after {
			content: attr(data-heading-content);
			position: absolute;
			background: #fff;
			display: block;
			padding-right: 1rem;
		}
	}

Men, det finns en stor nackdel med detta och det är att text i pseudoelement inte är markerbar.

CSS Grid to the rescue:

&.has-heading-line {
		display: grid;
		grid-template-columns: auto minmax(0, 1fr);
		align-items: center;
		grid-gap: 1rem;

		&::after {
			content: '';
			border-top: 1px solid var(--color-gray--darkest);
		}
}

Skapa ett default pattern för en custom post type med kod för hybridtema i WP

Skapa en fil som heter inc/pattern.{cpt}-template.php i ditt CPT-tillägg.

Lägg till template till dina $args när du registrerar din CPT:

'template' => array(
		array(
			'core/pattern',
			array(
				'slug' => 'erik/{cpt}-template',
			),
		),
	),

Registrera därefter en custom pattern category och ett block pattern (jag brukar lägga det rakt under register_post_type() ):

/* Register your custom category */
function erik_register_pattern_categories() {
	if ( function_exists( 'register_block_pattern_category' ) ) {
		register_block_pattern_category(
			'template',
			array( 'label' => __( 'template', 'erik-cpt'   ) )
		);
	}
}

/* Register your block Pattern */
function erik_register_patterns() {
	if ( function_exists( 'register_block_pattern' ) ) {

		ob_start();
		include( 'inc/pattern.{cpt}-template.php');
		$pattern_{cpt}_template = ob_get_clean();

		register_block_pattern(
			'erik/{cpt}-template',
			array(
				'title'       => __( '{cpt} template', 'ipm-patterns' ),
				'description' => _x( 'Template blocks setup for {cpt}.', 'Block pattern description', 'erik-cpt' ),
				'categories' => ['template'],
				'content'     => $pattern_{cpt}_template,
			)
		);
	}
}

Lägg till ditt mönster till filen pattern.{cpt}-template.php du skapade tidigare. Använd Gutenberg-redigeraren för att skapa mönstret och välj sedan Kopiera alla block från menyn (tre vertikala prickar) längst uppe till höger. Klistra sedan in detta i filen (exempel nedan). Kopiera inte över PHP-kommentarerna överst.

Glöm inte att byta ut {cpt} till namnet på din posttyp (eller valfritt).

Notera att man bara kan ha ett mönster per posttyp (vad jag vet) och om man ändrar mönstret i efterhand så kommer inte befintliga poster att uppdateras.

<?php
/**
 * Title: A block template for {cpt}
 * Slug: erik/{cpt}-template
 * Inserter: false
 */
?>

<!-- wp:columns {"style":{"spacing":{"margin":{"bottom":"var:preset|spacing|30"}}},"marginBottom":"mb\u002d\u002dx2"} -->
<div class="wp-block-columns" style="margin-bottom:var(--wp--preset--spacing--30)"><!-- wp:column {"width":"33.33%"} -->
	<div class="wp-block-column" style="flex-basis:33.33%"></div>
	<!-- /wp:column -->

	<!-- wp:column {"width":"66.66%"} -->
	<div class="wp-block-column" style="flex-basis:66.66%"><!-- wp:paragraph {"className":"is-style-default"} -->
		<p class="is-style-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sit amet dapibus nibh. Mauris congue gravida velit, quis maximus leo molestie ut. Phasellus non nibh nunc. Donec efficitur leo nec tempor pulvinar. Nulla viverra pellentesque ultrices. Nam malesuada maximus facilisis. Proin felis tellus, lobortis nec pellentesque eu, sagittis non purus. Vivamus porta viverra felis, ut ornare velit. Vestibulum euismod eros lacus, eget posuere leo feugiat vel.</p>
		<!-- /wp:paragraph --></div>
	<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:columns {"style":{"spacing":{"margin":{"bottom":"var:preset|spacing|30"}}}} -->
<div class="wp-block-columns" style="margin-bottom:var(--wp--preset--spacing--30)"><!-- wp:column -->
	<div class="wp-block-column"><!-- wp:image {"className":"full-width","fullWidth":true} -->
		<figure class="wp-block-image full-width"><img alt=""/></figure>
		<!-- /wp:image --></div>
	<!-- /wp:column -->

	<!-- wp:column -->
	<div class="wp-block-column"><!-- wp:image {"className":"full-width","fullWidth":true} -->
		<figure class="wp-block-image full-width"><img alt=""/></figure>
		<!-- /wp:image --></div>
	<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:group {"style":{"spacing":{"margin":{"bottom":"var:preset|spacing|40"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="margin-bottom:var(--wp--preset--spacing--40)"><!-- wp:image {"className":"full-width","fullWidth":true,"marginBottom":"mb\u002d\u002dx3"} -->
	<figure class="wp-block-image full-width"><img alt=""/></figure>
	<!-- /wp:image --></div>
<!-- /wp:group -->

Om du registrerar din posttyp i en klass så kör du patternfunktionerna i construct med:

public function __construct() {			
	add_action( 'init', array($this, 'erik_register_patterns') );
	add_action( 'init', array($this, 'erik_register_pattern_categories') );
}

Automatisk kalkylering av bredd på flexade kolumner baserat på antal och gap

.wrapper {
    --gap: 1rem;
    --columns: 1;
    display: flex;
    flex-wrap: wrap;
    gap: var(--gap);

    @media (min-width: 64em) {
        --gap: 2rem;
        --columns: 2;
    }
    @media (min-width: 100em) {
        --gap: 3rem;
        --columns: 3;
    }
    @media (min-width: 100em) {
        --gap: 3rem;
        --columns: 4;
    }

    .item {
        width: 100%;

        @media (min-width: 48em) {
            width: calc((100% / var(--columns)) - var(--gap) + (var(--gap) / var(--columns)));
        }
    }
}

Toggla show/hide med CSS

Flex

.wrapper {
  display: flex;
}

.inner {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease-out;
}

.wrapper.is-open .inner {
  max-height: 100%;
}

Just be sure there’s an extra div between the wrapper and the inner element:

<div class="wrapper">
  <div>
    <div class="inner">Expandable content</div>
  </div>
</div>

Grid

This one is a bit simpler, and probably a little more understandable. Make a CSS grid with a single grid item. All you have to do is transition grid-template-rows from 0fr to 1fr, so the grid item transitions to its auto height:

.wrapper {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s ease-out;
}

.wrapper.is-open {
  grid-template-rows: 1fr;
}

.inner {
  overflow: hidden;
}
<div class="wrapper">
  <div class="inner">Expandable content</div>
</div>

The one caveat to both of these is you can’t add any padding to the inner element. If you need padding, you have to add one more extra element inside the collapsed element and put the padding on that.

Max-height

.section {
  overflow:hidden;
  transition:max-height 0.3s ease-out; // note that we're transitioning max-height, not height!
  height:auto;
  max-height:600px; // still have to hard-code a value!
}
.section.collapsed {
  max-height:0;
}

There are two crucial downsides to this

One is obvious, and one is subtle. The obvious disadvantage is that we still have to hard-code a maximum height for the element, even if we don’t have to hard-code the height itself. Depending on your situation, maybe you can guarantee that you won’t need more height than that. But if not, it’s a pretty big compromise. The second, less obvious downside, is that the transition length will not actually be what you specify unless the content height works out to be exactly the same as max-height. For example, say your content is 600px tall, and your max-height is transitioning from 0px to 1000px with a duration of 1 second. How long will it take the element to get to 600px? 0.6 seconds! The max-height will continue transitioning, but the real height will stop changing once it reaches the end of its content. This will be even more pronounced if your transition is using a nonlinear timing function. If the transition is fast at the beginning and slow at the end, your section will expand quickly and collapse slowly. Not ideal. Still, transitions are relatively subjective, so in cases where this technique is otherwise appropriate, it could be an acceptable tradeoff.

Technique 2: transform: scaleY()

It’s important to note a couple of things about the nature of these transformations:

  1. They operate on the element’s visual representation as if it were simply an image, rather than a DOM element. This means, for example, that an element scaled up too far will look pixellated, since its DOM was originally rendered onto fewer pixels than it now spans.
  2. They do not trigger reflows. Again, the transform doesn’t know or care about the element’s DOM structure, only about the “picture” the browser drew of it. This is both the reason this technique works and its biggest downside.
.section {
  overflow:hidden;
  transition:transform 0.3s ease-out; // note that we're transitioning transform, not height!
  height:auto;
  transform:scaleY(1); // implicit, but good to specify explicitly
  transform-origin:top; // keep the top of the element in the same place. this is optional.
}
.section.collapsed {
  transform:scaleY(0); // *squish*
}

Add capability for editors to edit menus

/**
 * -------------------------------------------------------------------------
 * Add capability for editors to edit menus
 * -------------------------------------------------------------------------
 */
/*function add_theme_caps(){
	global $pagenow;
	// gets the author role
	$role = get_role('editor');

	if ('themes.php' == $pagenow && isset($_GET['activated'])) {
		// Test if theme is activated
		// Theme is activated
		// This only works, because it accesses the class instance.
		// would allow the editor to edit the theme options
		$role->add_cap('edit_theme_options');
	} else {
		// Theme is deactivated
		// Remove the capability when theme is deactivated
		$role->remove_cap('edit_theme_options');
	}
}
add_action( 'load-themes.php', 'add_theme_caps' );
*/

function hide_menu() {

	if (current_user_can('editor')) {
		$role = get_role('editor');
		if (!$role->has_cap( 'edit_theme_options' ) ) {
			$role->add_cap('edit_theme_options');
		}

		remove_submenu_page( 'themes.php', 'themes.php' ); // hide the theme selection submenu
		remove_submenu_page( 'themes.php', 'widgets.php' ); // hide the widgets submenu
		remove_submenu_page( 'themes.php', 'customize.php?return=%2Fwp-admin%2Fthemes.php' ); // hide the customizer submenu
		remove_submenu_page( 'themes.php', 'customize.php?return=%2Fwp-admin%2Fprofile.php' );
		remove_submenu_page( 'themes.php', 'customize.php?return=%2Fwp-admin%2Fnav-menus.php' );
	}
}

add_action('admin_head', 'hide_menu');

Varför man bör tänka till några gånger extra vid önskemål om “Sticky header”/”Sticky menu”

This fancy pattern hurts UX far more than it improves it.”

Adam Silver 

(Skrivit för Smashing Magazine, A List Apart och CSS Tricks. Jobbat med ex. GOV.UK, Just Eat, Tesco och BBC – vilket, om någon inte känner till ovanstående, är ett sjukt CV)

Problem #1: tar alltid upp plats

Är alltid i vägen för content. Än värre på mindre skärmar/upplösningar där ytan är än mer begränsad.

Problem #2: de skymmer innehåll

Hänger ihop med #1. De lägger sig ovanför övrigt innehåll.

Problem #3: kan gå sönder vid inzoomning

När man zoomar kan storleken på header/meny öka och på så vis tränga bort övrigt content. Eller så försvinner dom, förvrängs/”felalignar” eller kapas. 

Kan ev undvikas på vissa sätt men till kostnad av ganska mycket programmeringstid.

Problem #4: de verkar närmare än de är

Sticky menyer/headers är alltid visuellt tillgängliga men i verkligheten är de ofta väldigt långt bort för användare av tangentbord som navigerar med tabtangenten; eller de som på annat sätt navigerar webben.

Problem #5: de skymmer länkar och andra fokuserbara element

Besökare som navigerar tillbaka upp på sidan med tangentbord kan hamna i ett läge där fokus är skymt bakom sticky meny/header.

Problem #6: svåra att komma åt

Om sticky header/meny är högre än innehållet och viewport så kan man inte komma åt det nedersta på menyn i vissa webbläsare. Även om innehållet är högre än header/menyn så måste man fortfarande scrolla ner till botten på content för att se menyn.

Man kan addera en inre scrollbar till menyn men multipla scroll är svåra att hantera (https://baymard.com/blog/inline-scroll-areas). 

Problem #7: interna sidankare känns trasiga när de klickas på

Vissa sticky innehåller länkar som tar besökaren längre ner (eller upp) på sidan, sk. ankarlänkar.

Med en stickyfunktionalitet så är det inte säkert att man förflyttar sig vid klick och därför kan interfacet känns trasigt.

Vad ska du göra istället?

  1. Håll sidor korta: Sticky menyer/headers är ett symptom av (väldigt) långa sidor, åtgärda grundorsaken. Har du inte långa sidor så behövs inte heller sticky header/meny.
  2. Låt besökare skrolla(!): Det är en myt att skrollning är ett problem (och helt seriöst så började dom diskussionerna ebba ut redan för +10 år sedan). Även på telefoner så är toppen av sidan oftast en “flick” eller två ifrån (normalt sett kan man alltid trycka på “top bar:en” för att bli skickad tillbaka till toppen, i de flesta webbläsare).
  3. Lägg in relevanta länkar, i kontext: Exempelvis kan du lägga in länkar i löptext (det är trots allt själva definitionen av World Wide Web), lägga till formulär i slutet av en artikel eller kasta ut en CTA (länkad banner/knapp) här och var.

Var försiktig med att disable:a submit-knappar

Många gånger är man frestad att inaktivera en submitknapp för att man tänker sig att man då eliminerar fel eller åtminstone inte visar fel längs vägen (“error prevention is better than a well-designed error”). Men här är några saker man kan tänka på om man tänker så.

#1: Vi får ingen feedback

När fel görs i ett formulär utan feedback så behöver användaren scanna av varje fält, hitta ett eventuellt fel och sen komma på hur de ska lösa det. Kontra en validering på submit som tydligt markerar ut vart felet är och varför det blir fel.

#2: De kan få interfacet att kännas br0ken

Om användaren tycker att deras svar är okej så känns UI söndrigt “Hallå, jag har ju fyllt i allt?!”. Och om det finns flera fel och ett av dessa åtgärdas så fortsätter knappen att vara inaktiverad.

#3: Svåra att se

Inaktiverade knappar har generellt sett låg kontrast för att signalera att de är inaktiva. Detta gör dom svåra att läsa/se, speciellt för användare med synnedsättningar.

#4: Är inte fokuserbara (focusable)

Användare som navigerar med tangentbordet kan inte tabba till knappen. Och användare med dålig syn kan inte se knappen. En tydlig väg framåt saknas därför för dessa användare.

#5: De är missvisande

Inaktiverade knappar har generellt sett låg kontrast för att signalera att de är inaktiva. Men detta är inte alltid tydligt. Så vissa användare kommer ändå att försöka klicka på dom.

#6: Användare kanske inte märker att knappen blir aktiverad

Detta beror på att knappen kanske är “utanför skärmen” pga upplösning/skärmstorlek eller storlek på formuläret. Och om knappen är synlig så är användaren snarare fokuserad på att fylla i fälten, inte på att knappen byter status.

Att inaktivera submitknappen hindrar inte användare att göra fel. Men den kan hindra dom från att förstå att de gjort ett och hur de löser det.

Alternativ för betalning på Internet (med WordPress)

Priser hämtade nov 2023

Förr i tiden kunde man enkelt hänga på en köpknapp från Paypal, a la 

https://www.paypal.com/buttons/ men jag vet inte om det är någon höjdare numera.

Om man vill utgå från ett formulär så har två av de största premium-formulärbyggarna för WordPress (något begränsade) betalningsalternativ man kan hänga på: 

Gravity Forms

Basic $59 / år

Elite $259 / år

Pro $159 / år

2Checkout (Elite), Stripe (Pro), Paypal Checkout (Pro)

Ninja Forms

Plus $99/år

Paypal ingår

Pro $199/år

Stripe, Authorize.net och Elavon ingår

Notera att Stripe erbjuder ex Klarna Checkout(vet ej hur det fungerar)

Här finns även en uppstickare som verkar intressant:

WP Simple Pay

Klarna, Stripe, Google Pay, Apple Pay etc.

Från $49.50/år men kräver troligtvis $199.50 för att få ex Klarna

Easy Digital Downloads

Ett annat alternativ som påminner om WooCommerce (och är en otroligt stor lösning om man inte har fysiska produkter, tror det är den näst största i WP-sfären efter Woo) är Easy Digital Downloads.https://easydigitaldownloads.com/

Inkluderar Stripe och PayPal

Från €95/år – antagligen krävs dyrare nivå (€192 el. €288)

Precis som med WooCommerce bygger man ut med extra tillägg beroende på önskad funktionalitet. Woo har dock väldigt många fler (och fler svenskanpassade) lösningar enligt min uppfattning.

WooCommercebaserade lösningar

WooCommerce, världens största e-handelsverktyg och byggt som ett tillägg till WordPress. Man kan lägga upp en tjänst som en produkt. Det behöver inte vara fysiska produkter man säljer men det blir dock en skillnad i köpförfarandet. Man lägger produkt i varukorg kontra ett formulär med köpinfo (jämför Gravity, Ninja, WPSP ovan).

Betaltjänster (payment gateways) för WooCommerce

Paypal ingår (tror det är https://woo.com/products/woocommerce-paypal-payments/)

Manuella alternativ så som Gratis, Betala fysiskt (alltså en form av bokning), faktura/bankgiro ingår.

Klarna, Walley, Qliro, Worldline (tidigare Bambora), Svea m.fl. erbjuder färdiga gratis tillägg för att koppla ihop med WooCommerce. Jag tror att alla (osäker på Svea) innehåller bl.a. Swish som betalmetod.

Swish (för) handel

Redlight Media i Malmö erbjuder ett tillägg till WooCommerce för enbart Swishbetalning (960 kr/år). Notera att precis som med övriga betaltjänstleverantörer så tillkommer Swish egna kostnad. Kostnaderna skiljer sig åt beroende på ett antal faktorer som ex bank. Men ett prisexempel (lånat från Angry Creative så vet inte hur aktuellt det är 2023/2024) kan se ut så här, för att få en uppfattning:

WooCommerce: gratis

Swishtiullägg till WC: 960 kr/år

Uppläggningsavgift Swish: 1000 kr

Månads- eller årsavgift: ca 50 kr/mån alt. 500-1500 kr/år beroende på bank

Transaktionsavgift: 1-3 kr

Ungefärliga prisjämförelser

TjästKostnad
WooCommerce + Klarna, Walley, Qliro, Worldline etcGratis tillägg, betala för respektive tjänst (gissningsvis månadsavgift + transaktionsavgift)
WooCommerce + Swish Handel960 kr / år för swishtillägget + avtal med Swish (månadsavgift + transaktionsavgift)
Easy Digigtal DownloadsGissningsvis €192 / år + avtal med ex Klarna (månadsavgift + transaktionsavgift)
WP Simple PayGissningsvis $199,50 / år + avtal med ex Klarna (månadsavgift + transaktionsavgift)
Ninja FormsFrån $99/år (om Paypal funkar) men antagligen $199 / år + avtal med ex Stripe (transaktionsavgift + procentuell avgift – helt beroende på Stripelösning och om man ex har Klarna därigenom)
Gravity Forms$159 / år, då ingår PayPal Checkout eller Stripe. Tillkommer transaktionsavgift + procentuell avgift – helt beroende på Stripelösning och om man ex har Klarna därigenom

Jag tror att man tänka att det kostar som minst ~ 2 000 kr / år oavsett tjänst man väljer. Exklusive att jag rekommenderar att hålla sajt och tillägg ajour (gärna 3-4 gånger per år) för att undvika säkerhetshål, att något går sönder eller för den delen få nya features. Samt att det totalt tenderar att gå mindre tid vid mindre insatser ofta, än om man skulle göra en stor uppdatering vid färre tillfällen (1 gång/år eller mer sällan).

Prisexempel från Klarna är ruggigt svårt att hitta men jag har exempelpriser nedan men osäker hur gamla dessa är:

Fast avgift per transaktion: 3,50 SEK

Rörlig avgift på transaktionsvärde: 2.79%

Säljer man en produkt för 1 000 kr på ett år skulle det då innebära minst 2 000 kr för att använda tjänsterna samt strax över 30 kr i transaktionsavgifter (Klarna).

Data på internet – varför HTML är bättre än PDF

Varför krångla med att låsa in data i pdf-filer på webben när det enklaste och mest tillgängliga sättet är HTML?

HTML är ett märkspråk som tillsammans med TCP/IP och HTTP utgör den grundläggande standarden för WWW (World Wide Web); webbsidor skrivs i allmänhet som HTML och överförs över Internet med HTTP. Inget är helt enkelt mer lämpligt att ha på webben än HTML.

Det är en märklig företeelse att man även i modern tid envisas med att uppmana besökare att ladda ner pdf:er, ofta med mer data än vad som presenteras på sajten, när dagens webb har alla möjligheter att presentera data snyggt och smart.

Det är ännu märkligare om man väger in hur otroligt mycket av trafiken som kommer från mobiltelefoner och ”icke-persondatorer”. Mobiltelefoner som har ganska undermålig hantering av pdf-filer (Apple-produkter framförallt) och som försvårar att ta till sig informationen avsevärt. Inte särskilt användarvänligt någonstans.

Det finns egentligen bara en stark anledning till pdf: layout/arkdesign om pdf:en också ska bli exakt som man vill i print. Exempelvis om en säljare behöver skriva ut pdf. Finns detta behov är pdf ett bra komplement men då allra helst skapad on-demand direkt från PIM så man inte riskerar att visa gammal data.

Dock ska man ha med sig att om filen ska skapas dynamiskt så är det betydligt svårare att med kod sätta en snygg layout än vad man kan göra i InDesign, pga. diverse begränsningar beroende på de bibliotek man använder för att generera pdf:en. Det är också en enorm fördel om kund inte har helt orimliga (och helt ärligt väldigt konstiga) krav på exempelvis att allting ska få plats på ett A4-ark. Hanteras dynamisk data så öppnar det i de flesta fall dörren för variabla längder på ex. textdata osv. Har man några tusen pdf:er som genereras så kan man knappast inte ta hänsyn till om någon text är längre än den andra – det gäller istället att designa för fler sidor om så krävs. Inte minst om man blandar in andra språk, som tyska och finska.

Att låsa in data i PDF bör som minst ifrågasättas. Många gånger kanske man väljer att ha det ändå men då har det funnits en tanke bakom och man har svaret på “varför”. Det skulle kunna vara ”Vår säljorganisation vägrar att utvecklas och kräver att kunna gå in på sajten och skriva ut produktblad. Vad ska de annars faxa till varandra?” eller ”vi vet med hundra procents säkerhet att minst hälften av samtliga besökare surfar in hit för att ladda hem en pdf, vilket de är tvugna att ha pga x”.

Uppdatera inte din sajt!

Alla professionella sajtbyggare versionshanterar sin kod. Det innebär att all kod finns lagrad i ett så kallat repository. Fördelarna är många. Exempelvis kan vi se historik på varje enskild fil och kodrad, vi kan se vem som har skrivit vad, när och hur. Vi kan bygga nya funktioner i separata instanser som sedan kan läggas in när de är klara. Det gör det också väldigt enkelt för oss att sätta upp sajten i olika miljöer, såväl staging/utvecklingsserver och på våra lokala maskiner.

Fördelar med att inte själv uppdatera sin sajt och eventuella tillägg:

  • Vår versionshantering hamnar inte i osynk. Osynk gör att nästa insats från vår sida kommer att ta längre tid då vi behöver replikera de ändringar som gjorts, se till att allt fungerar och slå samman detta med repositoryt.
  • Vi har möjlighet att testa uppdateringarna i flera miljöer som inte är live/”produktionssajt”. Det är nämligen inte ovanligt att saker kan gå sönder. Speciellt tillägg kan vara klurigt då dessa utvecklas helt separat och oftast av andra personer än de som utvecklar plattformen. Det kan även röra sig om att utfasning av vissa funktioner gjorts vilket gör att vi kan behöva koda om sättet något fungerar eller presenteras på.
  • När vi säkerställt att sajten och alla funktioner fungerar kan vi enkelt bokföra ändringarna och skjuta upp uppdateringen live på bara några sekunder

Nackdelar med att själv uppdatera sin sajt och eventuella tillägg:

  • Dyrare om vi behöver hjälpa till med något
  • Sajten kan gå sönder i produktionsmiljö

För regelbundna uppdateringar föreslås ett samarbets-/förvaltningsavtal där man kan uppdatera vid givna tidpunkter/år. Exempelvis efter major release cycle på plattformen.

Fotnot: detta gäller även för nyinstallation av funktionalitet. I vissa fall med WordPress som plattform har vi varit med om installation av tillägg som man tror är bra och gör nytta med inte insett att tidigare tillägg har jobbat ihop med custom-kod för bättre nytta.

Exempel på projekt där saker gått sönder:

Sajt uppdaterades från 5.7 till 6.0 vilket innehöll en del breaking changes vilket gjorde att sajterna spottade ut felmeddelanden. Vilket krävdes att vi skrev om viss funktionalitet för att lösa problemet.

  • Kund hade låtit bli att uppdatera/underhålla sajten under många år samt ignorerat mail från Loopia (som hostade sajten) om att kunderna måste uppgradera PHP-version, annars kommer de till slut tvingas upp.
  • Loopia har som policy att bara stödja de tre senaste PHP-versionerna (major) då äldre versioner än så har nått EOL. Sajten var byggd långt innan och fungerade bara upp till PHP 7.4.
  • Sajten innehöll bärande tillägg och tema (templates) som byggdes i en tid då det inte krävdes licens för uppdateringar. När detta sedan ändrades saknades så aktiv licens och tillägg/tema kunde därför inte heller hämta in information om att det fanns uppdateringar. Och detta upptäcktes inte under 5-7 års tid = otroligt många uppdateringar.
  • När Loopia sedan tvingade upp lägsta PHP-versionen från 7.4 till 8.0 så var det så många saker som blivit föråldrade att sidan helt slutade att fungera (= helt vit sida utan innehåll).
  • Lösningen tog extra lång tid eftersom vi fick ställa ner mjukvaruversioner i en testmiljö för att se vad som gått fel, därefter hitta på en rimlig lösning (vilket blev att ta bort tillägg och tema och bygga om sajten med nytt tema) och sedan uppgradera systemversioner successivt.

Nackdelar med Magento

  • Begränsningar i vad som går att göra i CMS och shoppdelen utifrån kommunikations- och designperspektiv
  • Extremt undermåligt när det rör annat content än produktkatalog. CMS och DAM/Mediabibliotek är långt bakom konkurrenterna
  • Vill gärna ha LESS
  • Eventuellt begränsningar i system vad gäller generering av övrigt säljmaterial kontra ex möjligheterna i Pimcore

Varför man bör undvika att öppna undermenyer med “hover” (eller det klassiska uttrycket “mouseover”)

For decades, a common behavior for this kind of navigation is to open the menu on mouse hover. And for decades, a common user’s complaint about this pattern has been the absolute lack of certainty and control about how and when the sub-navigation opens and closes.”

Vitaly Friedman, Smashing Magazine (author, co-author and editor of all Smashing books and front-end/UX consultant working with European Parliament etc)

“I’ve built a bunch of websites and learned a lot more about usability, accessibility, and content strategy. Now, I find hover-triggered menus lacking on all those fronts.”

Mark Root-Wiley https://css-tricks.com/in-praise-of-the-unambiguous-click-menu/

Redan 2011 skrev UX Movement om detta fenomen https://uxmovement.com/navigation/why-hover-menus-do-users-more-harm-than-good/

Tyvärr är hover ganska vanligt men det bästa sättet att säkerställa att menyn fungerar över enheter, traditionella skärmar till tocuhskärmar, är click-to-open.

Man undviker då mycket frustration som kommer med hover, exempelvis:

Inkonsekvent UX på pekskärmar och för de som navigerar med tangentbord

Hovermenyer fungerar inte på pekskärmar. Det kan eventuellt lösas med kod för att upptäcka pekskärmar och växla till click-to-open. Men eftersom gränsen alltmer suddas ut mellan “vanliga” skärmar och skärmar med touch, kan dessa lösningar bli föråldrade. Här behöver man också ha med i beräkningen att lägga ut separata knappar för att öppna undermeny eftersom huvudmenyvalet i sig är en länk till just den sidan.

En del föredrar, eller kanske bara har möjlighet, att navigera mha tangentbordet. Normalt sett behöver man då tabba sig igenom alla länkar genom alla undermenyer fram till den länk du vill besöka (med klick hoppar man över underlänkar emn kan fälla ut med ENTER och tabba sig igenom om man vill).

Detta leder oss vidare till:

Frågor om vad som är klickbart

Med hovermenyer vet användare inte alltid om den överordnade länken är klickbar eller inte förrän de försöker klicka. Det är motsatsen till intuitivt. Ur exempelvis struktur- och SEO-pespektiv är det bra om den överordnade sidan finns och går att nå.

Övrig tillgänglighet 

Hover-menyer kan ge problem för användare som använder skärmläsare eller navigerar via tangentbord.

Narrow hover tunnel/The Buzz Wire Maze

Hover-tunnel är vägen en muspekare måste följa för att navigera medan menyn är öppen. Om detta är för smalt så är det svårt för besökaren att navigera rätt utan att den stänger sig. Tänk här också på personer med motoriska hinder eller personer som surfar med webbläsaren i zoomat läge.

Öppnad av misstag

En hovermeny är benägen att öppnas oavsiktligt om man navigerar över den på väg till något annat ställe på sidan. Eller för den delen stängs oavsiktligt för att man exempelvis råkar stöta till musen. Detta är än värre när det rör stora menyner och megamenyer där det tar lång tid att ta in allt innehåll i undermenyerna och räkna ut vart man ska navigera.

Inkonsekvent/otydlig

Är det överordnade objektet en länk eller inte?  Detta leder till mycket förvirring. Vissa hoppar rakt förbi användbara sidor på toppnivå, förutsatt att dessa objekt inte är länkar. Ännu andra antar att länkarna på toppnivån är sidor och försöker klicka på dem och blir frustrerade om så inte är fallet.

Aktivering av misstag

Att hovra är inte en avsikt att aktivera. Det är på sin höjd ett förslag att användaren kan aktivera något. Tekniskt sett hovrar användaren alltid. Detta är anledningen till att hovringstillstånd är fördelaktiga för användaren, till exempel byte av muspekare (webbläsare gör detta som standard) eller färg och linjering på länkar.

Med hover kan en användare av misstag öppna en meny trots att de aldrig tänkt det och när detta händer döljer det innehållet bakom och stör upplevelsen.

Om en användare avser att klicka på en länk på en sida kan användaren navigera iväg av misstag om menyn öppnas precis innan du klickar på den länken. Det låter sällsynt men är vanligare än man tror

Mixed input devices

Vi har bärbara och stationära datorer med pekskärm. Du kan också använda en mus eller styrplatta med ex. en iPad. Det är inte bara möjligt att använda touch på en stor skärm, och en mus på en liten skärm, dina besökare kan använda båda samtidigt. 

https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html

Klickfunktion är vad vi förväntar oss i de flesta andra sammanhang:

  • Använder du en touch/pekenhet? Hover finns inte
  • Använder du en applikationsmeny(t.ex. Arkiv, Redigera)? Dessa klickas fram
  • Använder du något annat än en mus? Att trycka på ENTER eller aktivera en länk med någon typ av växlingskontroll (switch/toggle) motsvarar mer att klicka än att :focus motsvarar :hover.

Oavsett din enhet eller inmatningsläge är ett ”klick” en mer universell och solid interaktion. Även med fördelen att klick/tap, alltså dator vs. telefon fungerar med samma beteende. Eftersom att klicka eller peka (tap) är en tydlig avsikt finns det ingen oavsiktlig aktivering/avaktivering.

Avoid using hover to expand dropdown lists. Hover is difficult for some users and won’t work on touch screens. Dropdowns should expand on click or with keyboard navigation.”

U.S. Web Design System’s(USWDS) navigation patterns

What it really boils down to is user intent. The purpose of a hover state is to indicate something is clickable(underlined text)… The purpose of a click is to actually do something, to take an explicit action. Opening a dropdown is an explicit action and should only happen on click.”

Bootstrap (reasons)

 

“Maybe you don’t even need submenus at all!” 

UK.gov design system

Deras menyer är endast länklistor, med on-page grids med länkar och dragspel accordions för att låta besökaren navigera.

Andra saker man kan göra/tänka på när man designar informationsarkitektur och menysystem:

  • Om < 4 objekt, undvik “dropdowns” och lägg ut det direkt (sparar användarinteraktioner).
  • Använd innehållsförteckning och/eller ämnessidor för att undvika “dropdowns”.
  • Multipla menyer på olika platser. Noggrant uttänkta och viktade efter vad det är, vad besökarna vill ha (mest) osv. Ex “VD-meny”, sidfotmeny och gärna kombinerat med föregående punkt.

https://medium.com/simple-human/why-hover-menus-are-problematic-b21d6c7de91c

Ladda CSS asynkront i WP

This HTML tricks let the browser to preload the CSS assets, and when it’s done load it as stylesheet so they take a lot of priority in the page.

add_filter('style_loader_tag', 'preload_gfont', 10, 2);

function preload_gfont($html, $handle) {

if ( $handle === 'google-fonts' || strpos( $handle, 'fontawesome' ) !== false ) {

return str_replace("rel='stylesheet'", "rel='preload' as='style' onload=\"this.rel='stylesheet'\"", $html);

}

return $html;

}

Woo: Reduce product stock conditionally

https://rudrastyh.com/woocommerce/reduce-product-stock-conditionally.html

Filter hook woocommerce_can_reduce_order_stock which returns either true or false depending whether product stock quantity can be decreased or not. But the amazing part is that this hook also has a second argument which is WC_Order object and it allows us to make any kind of conditions.

add_filter( 'woocommerce_can_reduce_order_stock', 'rudr_can_reduce_stock', 25, 2 );

function rudr_can_reduce_stock( $can_reduce, $order ) {
	
	if( Any kind of condition here ) {
		$can_reduce = false; // or true
	}
	
	return $can_reduce; // the default value
	

Don’t reduce product stock for orders with a specific custom field 

add_filter( 'woocommerce_can_reduce_order_stock', 'rudr_can_reduce_stock', 25, 2 );
function rudr_can_reduce_stock( $can_reduce, $order ) {
	
	if( 'META VALUE' === $order->get_meta( 'META KEY' ) ) {
		$can_reduce = false;
	}
	return $can_reduce;
	
}

Don’t reduce product stock if a specific product is in the order 

In this example please keep in mind that the product stock quanity will remain untouched for all products in the order.

add_filter( 'woocommerce_can_reduce_order_stock', function( $can_reduce, $order ) {

	$is_product_in_order = false;
	// get all order items
	$items = $order->get_items();
	if( $items ) {
		foreach ( $items as $item_id => $item ) {
			if( 'SPECIFIC PRODUCT ID' == $item->get_product_id() ) {
				$is_product_in_order = true;
				break;
			}
		}
	}

	return $is_product_in_order ? false : $can_reduce;
}, 25, 2 );

As you probably remember, products are stored in orders as order items.

Don’t reduce product stock for specific payment methods and specific order statuses 

This example is more practical one, I assume. Our goal here is to not decrease product stock quantities if order have been made without actual payment. So for example if “Cash on delivery” payment method was selected. Here we are stick to a specific order statuses:

  • processing – order has been made by a customer, in this case the product stock quantities remain unchanged.
  • completed – order has been approved by a shop manager in WooCommerce admin, now we are ready to decrease stock quantities. We will need one more action hook here which will be fired when order status is changed – woocommerce_order_status_changed.
add_filter( 'woocommerce_can_reduce_order_stock', 'rudr_no_stock_changes_for_cod', 25, 2 );
function rudr_no_stock_changes_for_cod( $reduce_stock, $order ) {

	if( $order->has_status( 'processing' ) && 'cod' === $order->get_payment_method() ) {
		$reduce_stock = false;
	}
	return $reduce_stock;

}

add_action( 'woocommerce_order_status_changed', 'rudr_reduce_stock_completed_orders', 20, 4 );
function rudr_reduce_stock_completed_orders( $order_id, $old_status, $new_status, $order ){

	// do nothing if this order was not not with cash on delivery payment method
	if( 'cod' !== $order->get_payment_method() ) {
		return;
	}

	// do nothing if it is not changing order status to "completed"
	if ( 'completed' !== $new_status ){
		return;
	}

	// reduce stock levels manually
	wc_reduce_stock_levels( $order_id );

}

Byt ut admin bar logo-länk

add_action( 'wp_before_admin_bar_render', 'change_admin_logo_link' );
 
function change_admin_logo_link() {
     
    global $wp_admin_bar;
     
    // Get the logo node
    $menu_node = $wp_admin_bar->get_node('wp-logo');
 
    // Change the link to wp-admin
    $menu_node->href = network_admin_url();
 
    // Add the modified node back
    $wp_admin_bar->add_node( $menu_node );
 
    // Clean up
    unset( $menu_node );
}

Ta bort inline style på Active Campaign embeds

/* Remove inline <style></style> added by Active Campaign embeds */
(function($) {

	$.fn.removeACStyle = function() {

		let acForm = $('._form');

		if (acForm.length == 0)
			return;

		acForm.siblings('style').remove();
		acForm.parent().siblings('style').remove();
	}

	$(function() {
		$('body').removeACStyle();
	});

})(jQuery);

Justera ACF Relationship field

function add_meta_field_to_acf_relation($title, $post, $field, $post_id){
    return str_replace('</div>', '</div> <strong>'.get_field('objectnumber',$post->ID).': </strong>', $title);
}
add_filter('acf/fields/relationship/result', 'add_meta_field_to_acf_relation',10,4)
add_image_size( 'acf-thumb', 21, 21, true );
function acf_relationship_thumbnails($title, $post, $field, $post_id){
if( in_array('featured_image', $field[‘elements’]) ) {
       $image = '';
      if( $post->post_type == 'attachment' ) {
           $image = wp_get_attachment_image( $post->ID, 'acf-thumb' );
        } else {
            $image = get_the_post_thumbnail( $post->ID, 'acf-thumb' );
     }
       $title = preg_replace("/(.*\<div class=\"result-thumbnail\"\>)(.*)(\<\/div\>.*)/", "$1$image$3", $title);
  }
   return $title;
}
add_filter('acf/fields/relationship/result', 'acf_relationship_thumbnails', 10, 4);